一个问题:双重检查加锁为什么用了 volatile 就可以正确工作?
反过来问:不用 volatile 修饰 resource
属性有什么问题?
双重检查加锁的简单示例:
public class DoubleCheckedLocking {
private static Resource resource;
public static Resource getInstance() {
if (resource == null) { // LL01
synchronized (DoubleCheckedLocking.class) {
if (resource == null) {
resource = new Resource();
}
}
}
return resource;
}
}
上述代码反编译后的字节码指令如下:
$ javap -c DoubleCheckedLocking.class
Compiled from "DoubleCheckedLocking.java"
public class net.coderbee.dcl.DoubleCheckedLocking {
public net.coderbee.dcl.DoubleCheckedLocking();
Code:
0: aload_0
1: invokespecial #10 // Method java/lang/Object."<init>":()V
4: return
public static net.coderbee.dcl.Resource getInstance();
Code:
0: getstatic #18 // Field resource:Lnet/coderbee/dcl/Resource;
3: ifnonnull 35
6: ldc #1 // class net/coderbee/dcl/DoubleCheckedLocking
8: dup
9: astore_0
10: monitorenter
11: getstatic #18 // Field resource:Lnet/coderbee/dcl/Resource;
14: ifnonnull 27
17: new #20 // class net/coderbee/dcl/Resource
20: dup
21: invokespecial #22 // Method net/coderbee/dcl/Resource."<init>":()V
24: putstatic #18 // Field resource:Lnet/coderbee/dcl/Resource;
27: aload_0
28: monitorexit
29: goto 35
32: aload_0
33: monitorexit
34: athrow
35: getstatic #18 // Field resource:Lnet/coderbee/dcl/Resource;
38: areturn
Exception table:
from to target type
11 29 32 any
32 34 32 any
}
最重要的是下面这几行,Java 里创建一个对象包含三个步骤:分配内存、执行初始化、赋值给引用。
17: new #20 // class net/coderbee/dcl/Resource
20: dup
21: invokespecial #22 // Method net/coderbee/dcl/Resource."<init>":()V
24: putstatic #18 // Field
由于重排序的原因,可能在初始化之前就把对象赋值给引用,导致访问的线程看到一个未初始化完成的对象。这就是双重检查加锁不用 volatile 的致命问题。
《Java并发编程实践》里提到的一些可能重排序的场景:
- 编译器生成的指令顺序可以与源码中的顺序不同。
- 编译器会把变量保存在寄存器中,而不是内存中。
- 处理器可以采用乱序或并行等方式来执行指令。
- 缓存可能会改变将写入变量提交到主内存的顺序。
- 保存在处理器本地缓存中的值,对其他处理器不可见。
结合 DoubleCheckedLocking
代码来看看什么情况下就会出现上述问题:
加入线程A调用 getInstance
方法,发现 resource 引用是空的,就进入同步块,再次判断发现 resource 还是为空,开始创建对象的第一步 分配内存,由于重排序,把新对象的赋值给了 resource,并刷新到了主存;此时另一个线程B 也调用 getInstance
方法,发现 resource 引用非空,直接开始访问 resouce 对象,它访问的对象可能是未初始化完成的。
加上 volatile 可能禁止指令重排序,要求给引用赋值必须在对象初始化完成之后。
synchronized 同步块可能会给人一种假象,以为解决了同步、可见性的问题,它确实可以解决这两个问题,前提是线程都经过同步块。
而双重检查加锁存在不经过同步块的问题。
同步块内的代码仍然是可以重排序的。
对于延迟初始化,推荐的是资源占位符类模式,示例如下:
public class ResourceFactory {
static class ResourceHolder {
static Resource resource = new Resource();
}
public static Resource getInstance() {
return ResourceHolder.resource;
}
}
欢迎关注我的微信公众号: coderbee笔记 。