java并发-对象的共享
java并发-对象的共享
贲大侠 发表于3个月前
java并发-对象的共享
  • 发表于 3个月前
  • 阅读 7
  • 收藏 0
  • 点赞 0
  • 评论 0

腾讯云 新注册用户 域名抢购1元起>>>   

摘要: 《Java并发编程实战》读书笔记

1.可见性

    在单线程环境中,如果向某变量写入值,然后在没有其他操作的情况下读取这个变量,那么总能的到相同的值,没毛病。但是在多线程环境下,当读、写操作在不同的线程中执行时候,得到的结果便很容易出现问题,因为在线程进行读操作的时候,很难看得到其他线程在写入值。所以为了确保可见性,可以使用同步机制。在  java并发-线程安全性 中的目录3有介绍过可见性问题的例子,可以看下。

1.1 volatile变量

    volatile变量可以确保一个变量在更新时用一种可预见的方式来告知其他线程。当一个域声明为volatile类型后,编译器运行时会监视这个变量,它是共享的,针对它的操作不会与其他的内存操作一起被重排序。volatile不会被缓存在寄存器或者对其他处理器隐藏的地方。(重排序:从JVM并发看CPU内存指令重排序(Memory Reordering))

    我们可以把volatile想象成如下代码:

	private int value;
	
	public synchronized int get(){    //把volatile变量的读操作看做为get()方法
		return value;
	}
	
	public synchronized void set(int value){ //把volatile变量的写操作看做为set()方法
		this.value = value;
	}

    把volatile的读写分别视为get()和set()方法。当然,这也只是想象,在访问volatile时不会进行加锁的操作,没了锁,就没了线程阻塞,所以可以把volatile看为更轻量级的同步机制。

    volatile提供了可见性,假设线程A写入一个volatile变量并且线程B随后读取该变量,在A写入volatile变量之前,volatile变量对A都是可见的,在B读取了volatile变量之后,对B也是可见的。也就是说从内存可见性的角度来看,写入volatile相当于退出同步,读取相当于进入同步。但是,volatile内存可见性上的作用不是最强的,以后会介绍更加牛逼闪闪的。

下面列一个volatile的经典用法:

private volatile boolean asleep;

public  void test(String str) {
	while (!this.asleep) {
         
	}
}

     检查asleep是否退出循环,当asleep被修改时,如果不是用volatile修饰的变量,执行线程在判断时无法发现当前asleep被修改为true。在使用volatile修饰以后,增强了可见性,就会避免一些问题的发生。

    volatile变量通常被当做是标识完成、中断、状态的标记使用。它也存在一些限制。比如用volatile修饰的变量在进行递增操作的时候是无法满足  操作 的原子性。volatile变量只能保证可见性,而加锁可以保证可见性和原子性。 

    只有满足下面所有的标准后,你才能使用volatile变量:

    1.写入变量时并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值

    2.变量不需要与其它的状态量共同参与不变约束

    3.访问变量时,没有其他的原因需要加锁

2.发布与逸出

    发布一个对象的意思是指,让对象能够在当前作用域之外被使用。比如将指向该对象的引用保存到其他代码可以访问的地方,或者在一个非私有的反方返回该引用,或者把引用当作参数传递到其他类的方法中。举个例子:

public class A {
	public static List<C> list;
	
	public void initialize(){
		list = new ArrayList<>();
	}
	
	public List<C> getList(){//在一个非私有的反方返回该引用
		return list;
	}
	
	public void setBList(){
		B b  = new B();
		b.setList(list);//把引用当做参数传递到其他类的方法中
	}
}

    在很多情况下,我们需要确保对象以及其内部状态不被发布。而有时候又要发布某个对象,想要确保线程安全,则可能需要同步。发布内部状态可能会破坏封装性,难以维持程序的不变性条件。如果在对象构造完之前就发布该对象,就破坏了线程安全性。如果将一个C对象添加到list中,那么C也被发布了,任何代码都可以遍历这个list来获取C的引用。

    当一个不该被发布的对象被发布的时候,这种情况被称之为逸出。

public class D {
	private String[] str = new String[] { "a", "b" };//私有的String数组

	public String[] getStr() {//公共方法把私有的数组发布了,尴尬不。
		return this.str;
	}
}

    上面的代码 方法getStr()发布了 str数组,也就是说任何调用getStr()方法的代码都有可以修改str数组内的内容,这种情况就是逸出,str已经逸出了所在的作用域,一个私有的数组被发布了以后可以被任何调用getStr()的代码蹂躏,毫无尊严可言。在发布一个对象的时候,只要是不是私有的都会被发布。

    还有一种this逸出就是在发布一个内部的类实例的时候:

public class ThisEscape {
	public ThisEscape(EventSource source) {
		source.registerListener(new EventListnener() {
			public void onEvent(Event e) {
				doSomething(e);
			}
		});
	}
}

    在ThisEscape的构造方法进行初始化的时候,registerListener执行完毕后发布了一个EventListnener对象,这个时候已经是this逸出了,问题在于初始化还没完毕。也就是从ThisEscape的构造函数中发布对象时,只是发布了一个尚未构造完成的对象。

2.1 安全的对象构造过程

public class ThisEscape {
	private final EventListnener listener;//在ThisEscape的作用域内声明私有的引用listener

	public ThisEscape() {//构造方法初始化
		listener = new EventListnener() {
			public void onEvent(Event e) {
				doSomething(e);
			}
		};
	}
	
	public static ThisEscape newInstance(EventSource source){
		ThisEscape escape = new ThisEscape();//这一行跑完,保证escape的构造方法初始化完毕,是一个完整的对象
		source.registerListener(escape.listener);//在进行registerListener()的发布
		return escape;
	}
}

    把ThisEscape的构造方法定位私有的,其次构造未完成时不进行发布,然后定义一个工厂方法,在工厂方法 new一个ThisEscape,ThisEscape 的构造方法执行完毕后,在进行registerListener的发布,来防止this逸出。

    简单地说上述代码就是:不要在ThisEscape构造初始化的时候发布(即调用registerListener),设立工厂方法的作用就是为了保证ThisEscape构造完毕后在发布。

3.线程封闭

    当访问共享可变数据时,通常需要加同步。一种避免使用同步的方式就是不共享数据。这种技术被称之为线程封闭,它是实现线程安全性最简单的方式之一。当某个对象被封闭在一个线程的时候,这种方法自动实现线程安全线,即使这个对象不是线程安全的。

3.1 ThreadLocal

    维持线程封闭性的一种规范的方式是使用ThreadLocal,简单的说这个类能够使线程中的某个值与保存值的对象关联起来。其中有get和set的方法,这些方法为每个使用该变量的线程都存有一份独立的副本,互不干扰,从而达到一种隔离的效果。

3.1.1 使用

    ThreadLocal对象通常用户防止可变的变量进行共享,举个栗子:

public class TestThreadLocal {
	private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>(){
		@Override
		protected Integer initialValue() {
			return 0;//ThreadLocal value初始化值为0
		}
	};
	
	
	public  static void main(String[] args) {
		for(int i=0;i<5;i++){
			new Thread(new MyThread(i)).start();
		}
	}
	
	static class MyThread implements Runnable{
		private int i ;
		
		public  MyThread(int i) {
			this.i = i;
		}
		
		@Override
		public  void run() {
			System.out.println("线程"+i+"初始value:"+value.get());
			for(int x=0; x<10;x++){
				value.set(value.get()+x);//给当前线程set变量值
			}
			System.out.println("线程"+i+"累计后value:"+value.get());
		}
		
	}
}

    结果:

线程0初始value:0
线程0累计后value:45
线程1初始value:0
线程1累计后value:45
线程2初始value:0
线程2累计后value:45
线程4初始value:0
线程4累计后value:45
线程3初始value:0
线程3累计后value:45

    就像上面说的,每个使用该变量的线程都存有一份独立的副本,无论什么操作都是互不干扰的。达到了线程内部的隔离效果。

    再举个大家比较熟悉的栗子:

public class TestThreadLocal2 {
	private static ThreadLocal<Connection> local = new ThreadLocal<Connection>(){
		protected Connection initialValue() {
			return DriverManager.getConnection(DBURL);
		};
	};

	public static Connection getConnection(){
		return local.get();
	}
}

    单线程应用程序中可能会维持一个全局的数据库连接,启动时会初始化这个链接,从而避免在调用每个方法时都要传递Connection对象。但是JDBC的链接对象不一定是线程安全的,如果多线程程序在没有同步的情况下使用全局的Connection对象时候,线程安全性就是个问题,这时候如果把Connection存在ThreadLocal里的话,让每个线程都拥有自己的链接,那么就安全多了。

    当某个频繁执行的操作要加一个临时对象,同时又希望每次执行时不重复分配的时候,也可以用到ThreadLocal。

3.1.2 原理解析

    先看下ThreadLocal的源码

protected T initialValue() {
        return null;
}

    initialValue()默认返回当前线程的初始值,默认为null。可以在实例化的时候初始化initialValue。

    ThreadLocal的构造方法啥也没做。

public ThreadLocal() {

}

    ThreadLocal最重要的两个方法就是get()和set(),让每个线程都存有一份独立的副本,互不干扰,从而达到一种隔离的效果。

    set()和get()实现涉及到一下几个类:

    Thread 线程类,代表一个线程

    ThreadLocalMap    可以把它看成一个hashmap,但和java.util.Map没什么关系,只是跟hashmap实现方式类似,都是采用哈希表的方式进行存储。

    ThreadLocalMap.Entry:把它看成是保存键值对的对象。

3.1.2.1 ThreadLocal.set()解析

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    set()方法的整体流程很简单,先通过Thread.currentThread()获得当前线程的引用,然后获取当前ThreadLocalMap的实例map,判断map是否为空,不为空的话,以当前threadlocal为key,再把value传入,如果为空则创建一个ThreadLocalMap,把键和值保存在其中。

    整体流程讲完,深入一下ThreadLocal,ThreadLocalMap,Thread之间的关系,它们是如果工作的。可以看到set()方法刚进入的时候,获取到了当前线程的引用也就是Thread t = Thread.currentThread()方法,这个方法是Thread的方法,那么进入Thread看下源码:

    /**
     * Returns a reference to the currently executing thread object.
     *
     * @return  the currently executing thread.
     */
    public static native Thread currentThread();

    Thread.currentThread直接返回了当前线程,那么回到ThreadLocal往下看,ThreadLocalMap map = getMap(t);这里根据当前线程获取了当前ThreadLocalMap的实例map,那么看下getMap(t)是怎么实现的:

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
}

    可以看到getMap直接返回了当前线程的threadLocals,那么这个threadLocals是什么呢,再看下Thread的源码:

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    Thread在初始化的时候就声明了一个ThreadLocalMap引用,也就是每个线程在创建的时候都会有一个ThreadLocalMap的引用。这里才是关键的地方,简单的说之所以能够让每个线程都实现从内部隔离,单独副本就是因为每个线程在创建的时候就有了自己的ThreadLocalMap。

    画面再回到ThreadLocal的set源码,当map为空时候创建一个ThreadLocalMap,接下来就很好理解了,直接调用createMap(t, value)为当前线程创建一个ThreadLocalMap对象用来存储:

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

3.1.2.2 ThreadLocal.get()解析

    解析完set(),get()就好理解多了,看下get源码:

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

    get()的整体流程:先通过Thread.currentThread()获得当前线程的引用,然后获取当前ThreadLocalMap的实例map,判断map是否为空,不为空的话,map调用getEntry(this)来获取当前保存键和值的对象,在对象中获取值来返回存储的值。如果为空,则调用一个初始化set的方法setInitialValue(),setInitialValue()获取了当前ThreadLocal的初始化默认值,创建一个ThreadLocal进行存储。

    这里有必要看下setInitialValue()这个方法,看下源码:

private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

    可以看到其实这个初始化set()的方法就是获取了当前ThreadLocal的初始值,然后判断了ThreadLocal  是否为空,不为空把初始值放入map,为空直接创建个ThreadLocalMap然后再存储初始值。

    以上就是ThreadLocal的存取机制。

4.不变性

    只要对象的状态不改变,保持不变性,那么对象一定是线程安全的。

    当对象满足一下条件的时候,对象才是不可变的:

    1.对象创建以后其状态就不能修改。

    2.对象的所有域都是final类型

    3.对象都是正确创建的,在对象创建期间this引用没有逸出

    4.对所有的变量不提供set方法。

    5.所有变量都是私有的

4.1 final

    final是java中的保留关键字,可以适用于类,方法,变量。一旦引用声明为final,改引用将不能被改变。 

4.1.1 final变量

    如果将变量声明为final变量,那么这个变量是只读的。

public class F {
	private final int i = 0;
	public void a() {
		i++;//编译器会在这一行报错 The final field F.i cannot be assigned
	}
}

4.1.2 final方法

    如果在方法前加上final证明这个方法不可以被子类的方法重写。

public class F {
	public final void test() {
		System.out.println("666");
	}

	class F1 extends F {
		@Override
		public void test() {   
			// 方法报错
			// Multiple markers at this line
		    // - Cannot override the final method from F
			// - overrides com.ben.test1.F.test
		}
	}
}

4.1.3 final类

    使用final来修饰的类叫做final类,final类除了不能被继承以外,其余都和普通类一样。

public final class F {
	public final void test() {
		System.out.println("666");
	}

	class F1 extends F { //The type F1 cannot subclass the final class F
	}
}

     创建不可变类就需要使用final关键字,对象一旦创建便不可以修改,且需要满足上面不变性的条件。但是对于集合对象,如果声明为final类型,那么是可以进行删除,增加等操作的,如果想要保证不变性,那就要对类进行“正确的构造对象”,比如:    

public final class F {//final类
	private final List<String> list = new ArrayList<>();
	
	public F() { //正确滴构造对象
		list.add("1");
		list.add("2");
		list.add("3");
		list.add("4");
	}
	
	public List<String> getList(){ //只对外提供get方法
		return list;
	}
}

5.安全发布

//不安全发布
public Holder holder;

public void init(){
      holder = new Holder(10);
}

public class Holder {
	private int n;
	public Holder(int n) {
		this.n = n;
	}
	
	public void assertSanity() {
		if(n!=n) {
			throw new AssertionError("This statement is false.");
		}
	}
}

    上述代码比较有疑惑的地方就是Holder里的  if(n!=n) 看起来屌,n怎么可能不等于n呢,其实却是有可能不等于n。假设有两个线程A和B,A调用了init(),B调用了assertSanity(),那么此时会出现这样两种情况:

    首先有可能会出现由于线程A调用的init()还没有创建完毕对象,线程B调用了assertSanity(),也就是说线程B看到了线程A还未创建完毕的对象,那么结果肯定会报出空指针的问题。

    其次在A调用了init(),如果Holder的构造方法还没构造完毕,B就调了assertSanity(),那么此时Holder的n变量就是0,在if(n!=n)时,第一次获取n为0,而第二次获取n变量的时候,这时候构造方法执行完毕了,n变成了10,那么结果很明显,尴尬了。

    我们可以改造下代码:

public valetile Holder holder; //使用volatile声明来保证holder的更新状态是随时可见的。

public void init(){
      holder = new Holder(10);
}

public class Holder {
    //使用final来防止n在构造方法没有执行完毕被赋值为0的情况(用final声明的变量是必须等待构造完毕才能使用的 嘿嘿嘿...)
	private final int n;

	public Holder(int n) {
		this.n = n;
	}
	
	public void assertSanity() {
		if(n!=n) {
			throw new AssertionError("This statement is false.");
		}
	}
}

                                  

共有 人打赏支持
粉丝 0
博文 11
码字总数 12261
×
贲大侠
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: