序
最近在读Java并发编程的艺术这本书感慨挺多的,今天单独挑出一个问题写一下心得
学习到的
整本书学习到目前,印象最深得就是书作者一直在说的happens-before
,具体意思参照如下:happens-before
(先行发生原则)是一种定义两个操作之间执行顺序的规则和约束。它用于确定在多线程或并发环境中,一个操作是否应该在另一个操作之前发生,以保证程序执行的正确性和可见性。最简单的例子,在进行编程的过程中,如果某个操作比如初始化、赋值等一系列操作我们称为A操作,还有一系列操作如读取这些变量我们称为B操作,如果A是happens-before
B的那么A的一系列操作在B看来是已经执行完的。
典型的问题
书中举到的一个一直在使用的一个例子,双重检查锁的单例模式实现
public class DoubleCheckedLocking {
private static Instance instance;
private DoubleCheckedLocking() {}
public static Instance getInstance() {
if (instance == null) {
synchronized (DoubleCheckedLocking.class) {
if (instance == null) {
instance = new Instance();
}
}
}
return instance;
}
}
这段代码,刚开始的时候我也没有看出问题,为了提升在多线程中的性能,在第一次进入方法的时候先进行一次是否实例为空的校验,为空的时候才对整个类进行加锁,然后再进行校验和创建的过程,整个出问题的地方根源就在于instance = new Instance()
这里,先说出这里的问题就是代码读到instance部位null的时候,可能instance引用的对象还没有完成初始化,下面我们具体分析
问题分析
我们简化创建对象的过程,伪代码如下
memory = allocate() // 分配内存空间
constructInstance(memory) // 进行初始化
instance = memory // 将实例指向分配的地址
这简化的三行就可能会涉及到指令重排的一个问题,只要重排序不会影响单线程内的执行结果,就可以进行重排序,这段创建的过程结束后就是为了给线程读取instance
变量,所以上述代码中2、3行是可以进行重排序,这时代码就会变成如下
memory = allocate() // 分配内存空间
instance = memory // 将实例指向分配的地址
constructInstance(memory) // 进行初始化
这样是不会影响程序的执行结果,最终还是读取到相应的instance
但这样在单个线程内是没问题的,涉及到多个线程后就会出现问题,具体原因如下
线程B这时会看见一个还未初始化的对象,最终的解决方法,书中也是给了两个,第一种是不允许2、3行操作重排序,第二种是允许继续进行重排序,但别的线程不可见。
所以最简单的解决方法也就是对该对象变量使用volatile
进行修饰,会禁止指令重排,且保持最新的可见。
理解
阅读这本书到现在为止,给我感触最深的就是,多线程的并发艺术本质就是多线程间的通信,如何有效、高效、合理的进行通信,还有一点就是,控制执行顺序,同时java基本是通过内存进行通信的,这就在某些场景下有更严格的要求,是保证是任何线程对内存的更改对其他线程是可见的且是最新的。