Java并发编程(2)-线程安全之共享对象解读

文章目录

      • 一、变量可见性
        • 1.1、共享对象的可见性
        • 1.2 、volatile变量
      • 二、变量发布与逸出
        • 2.1、如何发布对象
        • 2.2、发布对象带来的this引用逸出
      • 三、线程封闭
        • 3.1、Ad-hoc 线程限制
        • 3.2、使用 ThreadLocal
      • 四、如何安全地共享对象


更多关于Java并发编程的文章请点击这里:Java并发编程实践(0)-目录页


在上一篇关于线程安全概述的文章中提到过,编写正确的并发程序的关键在于对共享的、可变状态的变量进行访问管理,上一篇着重讲解使用同步来避免多个线程在同一时间访问同一数据;这一篇将讲解共享和发布对象的技术,使多个线程能够安全地访问他们。
本文内容均总结自《Java并发编程实践》第三章 共享对象 章节的内容 ,详情可以查阅该书。

一、变量可见性

1.1、共享对象的可见性

阅读下面的代码,运行main方法后,会出现什么结果?

public class ThreadTest{
	private static  boolean ready;
	private static  int i;
	
	public static void main(String[] args) {
		new MyThread().start();
		i = 1;
		ready = true;
	}
	
	public static class MyThread extends Thread{
		public void run(){
			if(!ready){
				Thread.yield();
			}
			System.out.println(i);
		}
	}
}

main方法是写入数据的线程,而MyThread是读取数据的线程,由于没有适当同步操作,两个线程之间发生了读写的分离,而通常来说,当读和写分离为两个线程时,不能保证读线程能及时地读取到其他线程写入的值,甚至说不可能。为了保证读写线程的值能共享,必须使用同步机制。

1.2 、volatile变量

锁不仅仅是关于同步和互斥的,也是关于内存可见的。为了保证所有的线程都能够看到其共享的、可变变量的最新值,读取和写入线程必须使用公共的锁进行同步。
Java语言也提供了其他的选择,即一种同步的弱形式:volatile变量。它确保变量的更新以可预见的方式告知其他线程。当一个域声明为volatile类型后,编译器与运行时会监视这个变量;volatile变量不会缓存在寄存器或者缓存在对其它处理器隐藏的地方。所以,读一个volatile类型的变量时,总会返回由某一线程所写入的最新值

以下的数羊程序简单说明volatile变量。

private volatile boolean asleep;
	//是羊则数羊
	while(!asleep){
		countSomeSheep();
	}

volatile变量通常被当做标识完成、中断、状态的标记使用,如上面程序中的asleep标记。尽管volatile也可以用来标识其他类型的状态信息,但是必须十分小心。比如,volatile的语义不足以使自增查找(count++)原子化,除非你能保证只有一个线程对变量执行写查找。原子变量(atomic variable)提供“读-写-改”的原子查找的支持,而且常被用作更优的volatile变量

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

写入变量时并不依赖变量的当前值;或者能够保证只有单一的线程改变的值;

变量不需要与其他的状态变量共同参与不变约束;

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

二、变量发布与逸出

发布一个对象的意思是使它能够被当前范围之外的代码所使用。比如将一个引用存储到其他代码可以访问到的地方。

2.1、如何发布对象

最常见的发布对象方式是将对象的引用存储到公共静态域中。任何类和线程都能够看到这个域:

public static Set<Secret> knownSecrets;
	public void initialize(){
		knownSecrets = new HashSet<Secret>();
}

2.2、发布对象带来的this引用逸出

并发编程实践中,this引用逃逸("this"escape)是指对象还没有构造完成,它的this引用就被发布出去了。这是危及到线程安全的,因为其他线程有可能通过这个逸出的引用访问到“初始化了一半”的对象(partially-constructed object)。这样就会出现某些线程中看到该对象的状态是没初始化完的状态,而在另外一些线程看到的却是已经初始化完的状态,这种不一致性是不确定的,程序也会因此而产生一些无法预知的并发错误。( 详情可阅读《Java并发编程实践》)

三、线程封闭

访问共享的、可变的数据要求使用同步。一个可以避免同步的方式就是不共享数据。如果数据仅在单线程中被访问到,就不需要任何同步。线程封闭(Thread confinement)技术是在实现线程安全的最简单的方式之一。当对象封闭在一个线程中时,这种做法会自动成为线程安全的。

3.1、Ad-hoc 线程限制

Ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序实现来承担。Ad-hoc线程封闭是非常脆弱的,因为没有任何一种语言特性,例如可见性修饰符或局部变量,能将对象封闭到目标线程上。事实上,对线程封闭对象(例如,GUI应用程序中的可视化组件或数据模型等)的引用通常保存在公有变量中。(详情可阅读《Java并发编程实践》)

3.2、使用 ThreadLocal

一种维护线程限制的更加规范的方式是使用TheadLocal,它允许将每个线程与持有数值的对象关联在一起。ThreadLocal提供了get和set访问器,为每个使用它的线程维护一份单独的拷贝。所以get总是返回由当前执行线程通过set设置的最新值。(详情可阅读 http://www.cnblogs.com/dolphin0520/p/3920407.html)

四、如何安全地共享对象

为了安全地发布对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确创建的对象可以通过下列条件安全地发布:

通过静态初始化器初始化对象的引用;

将它的引用存储到volatile域或AtomicReference;

将它的引用存储到正确创建的对象的final域中;

将它的引用存储到由锁保护的域中。

在并发程序中,使用和共享对象的一些有效策略:

线程限制:一个线程限制的对象,通过限制在线程中,而被线程独占,并且只能被占有它的线程修改。

共享只读:一个共享的只读对象,在没有额外同步的情况下,可以被多个线程兵法地访问,但是任何线程都不能修改它。

共享线程安全:一个线程安全的对象在内部进行同步,所以其他线程无须额外同步,就可以通过公共接口随意地访问它。

被守护的:一个被守护的对象只能通过特定的锁来访问。

你可能感兴趣的:(Java并发编程(2)-线程安全之共享对象解读)