更多关于Java并发编程的文章请点击这里:Java并发编程实践(0)-目录页
在上一篇关于线程安全概述的文章中提到过,编写正确的并发程序的关键在于对共享的、可变状态的变量进行访问管理,上一篇着重讲解使用同步来避免多个线程在同一时间访问同一数据;这一篇将讲解共享和发布对象的技术,使多个线程能够安全地访问他们。
本文内容均总结自《Java并发编程实践》第三章 共享对象 章节的内容 ,详情可以查阅该书。
阅读下面的代码,运行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是读取数据的线程,由于没有适当同步操作,两个线程之间发生了读写的分离,而通常来说,当读和写分离为两个线程时,不能保证读线程能及时地读取到其他线程写入的值,甚至说不可能。为了保证读写线程的值能共享,必须使用同步机制。
锁不仅仅是关于同步和互斥的,也是关于内存可见的。为了保证所有的线程都能够看到其共享的、可变变量的最新值,读取和写入线程必须使用公共的锁进行同步。
Java语言也提供了其他的选择,即一种同步的弱形式:volatile变量
。它确保变量的更新以可预见的方式告知其他线程。当一个域声明为volatile类型后,编译器与运行时会监视这个变量;volatile变量不会缓存在寄存器或者缓存在对其它处理器隐藏的地方。所以,读一个volatile类型的变量时,总会返回由某一线程所写入的最新值。
以下的数羊程序简单说明volatile变量。
private volatile boolean asleep;
//是羊则数羊
while(!asleep){
countSomeSheep();
}
volatile变量通常被当做标识完成、中断、状态的标记使用,如上面程序中的asleep标记。尽管volatile也可以用来标识其他类型的状态信息,但是必须十分小心。比如,volatile的语义不足以使自增查找(count++)原子化,除非你能保证只有一个线程对变量执行写查找。原子变量(atomic variable)提供“读-写-改”的原子查找的支持,而且常被用作更优的volatile变量。
只有满足了下面所有标准后才能使用volatile变量:
写入变量时并不依赖变量的当前值;或者能够保证只有单一的线程改变的值;
变量不需要与其他的状态变量共同参与不变约束;
访问变量时,没有其他的原因需要加锁。
发布一个对象的意思是使它能够被当前范围之外的代码所使用。比如将一个引用存储到其他代码可以访问到的地方。
最常见的发布对象方式是将对象的引用存储到公共静态域中。任何类和线程都能够看到这个域:
public static Set<Secret> knownSecrets;
public void initialize(){
knownSecrets = new HashSet<Secret>();
}
并发编程实践中,this引用逃逸("this"escape)是指对象还没有构造完成,它的this引用就被发布出去了。这是危及到线程安全的,因为其他线程有可能通过这个逸出的引用访问到“初始化了一半”的对象(partially-constructed object)。这样就会出现某些线程中看到该对象的状态是没初始化完的状态,而在另外一些线程看到的却是已经初始化完的状态,这种不一致性是不确定的,程序也会因此而产生一些无法预知的并发错误。( 详情可阅读《Java并发编程实践》)
访问共享的、可变的数据要求使用同步。一个可以避免同步的方式就是不共享数据。如果数据仅在单线程中被访问到,就不需要任何同步。线程封闭(Thread confinement)
技术是在实现线程安全的最简单的方式之一。当对象封闭在一个线程中时,这种做法会自动成为线程安全的。
Ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序实现来承担。Ad-hoc线程封闭是非常脆弱的,因为没有任何一种语言特性,例如可见性修饰符或局部变量,能将对象封闭到目标线程上。事实上,对线程封闭对象(例如,GUI应用程序中的可视化组件或数据模型等)的引用通常保存在公有变量中。(详情可阅读《Java并发编程实践》)
一种维护线程限制的更加规范的方式是使用TheadLocal,它允许将每个线程与持有数值的对象关联在一起。ThreadLocal提供了get和set访问器,为每个使用它的线程维护一份单独的拷贝。所以get总是返回由当前执行线程通过set设置的最新值。(详情可阅读 http://www.cnblogs.com/dolphin0520/p/3920407.html)
为了安全地发布对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确创建的对象可以通过下列条件安全地发布:
通过静态初始化器初始化对象的引用;
将它的引用存储到volatile域或AtomicReference;
将它的引用存储到正确创建的对象的final域中;
将它的引用存储到由锁保护的域中。
在并发程序中,使用和共享对象的一些有效策略:
线程限制:一个线程限制的对象,通过限制在线程中,而被线程独占,并且只能被占有它的线程修改。
共享只读:一个共享的只读对象,在没有额外同步的情况下,可以被多个线程兵法地访问,但是任何线程都不能修改它。
共享线程安全:一个线程安全的对象在内部进行同步,所以其他线程无须额外同步,就可以通过公共接口随意地访问它。
被守护的:一个被守护的对象只能通过特定的锁来访问。