Java 双检锁问题

来源:互联网 发布:移动4g 网络 编辑:程序博客网 时间:2024/06/10 22:22

来源:《The "Double-Checked Locking is Broken" Declaration》

 

1. 单例模式的简单实现

// 只支持单线程的版本class Foo {    private Helper helper = null;    public Helper getHelper() {        if (helper == null)             helper = new Helper();        return helper;    }}

在多线程的情况下,可能会生成多个Helper实例。

 

 

2. 同步getHelper方法

// 支持多线程的版本class Foo {    private Helper helper = null;    public synchronized Helper getHelper() {        if (helper == null)             helper = new Helper();        return helper;    }}

 getHelper()方法被标记为synchronized后,JVM会只允许同时只有一个线程能执行getHelper方法。所以不会产生多个Helper实例。但是同步的开销会导致多线程执行效率降低。

 

 

3. 一种错误的双检锁方法

// 一种错误的双检锁方法class Foo {    private Helper helper = null;    public Helper getHelper() {        if (helper == null)             synchronized(this) {                if (helper == null)                     helper = new Helper();            }        return helper;    }}

 为什么是错的?

原因:在 helper = new Helper(); 这句中有两个主要操作。一是创建Helper的一个实例,二是将这个新创建的Helper实例的引用赋给helper这个字段。编译器规定这两个操作的顺序可能是先赋值实例的引用,后执行Helper实例内部的初始化构建。这可能导致另一线程在调用getHelper()方法时,因为helper字段不为null,所以开始使用helper所指的实例,而该实例的初始化工作却仍在前一线程中处于未完成状态,这就导致第二个线程使用了一个“坏”的Helper。

即使编译器事先规定了先构建实例后赋值,在一个多处理器系统中,处理器和内存系统还是有可能颠倒这两个操作的顺序。

 

4. 另一种错误的双检锁方法

// 另一种错误的双检锁方法class Foo {    private Helper helper = null;wei    public Helper getHelper() {        if (helper == null) {            Helper h;            synchronized(this) {                h = helper;                if (h == null)                     synchronized (this) {                        h = new Helper();                    }  // 释放内部的 synchronized 锁                helper = h;            }        }        return helper;    }}

 为什么是错的?

原因:退出监控(monitorexit)(如释放 sychronized 锁)的规则是“在 monitorexit 前的操作必须在释放锁之前执行”。但是没有规则保证“在 monitorexit 后的操作必须在释放锁之后才能执行”。也就是说编译器可能会把 helper = h; 这句移到内部的 synchronized 代码块内。这就又回到了3中的情况,即其它线程可能拿到一个“坏”的未完工的Helper实例。

注:在.Net CLR中情况有所不同。在CLR中,任何锁方法的调用都构成了一个完整的内存栅栏,在栅栏之前写入的任何变量都必须在栅栏之前完成;在栅栏之后的任何变量读取都必须在栅栏之后开始。

 

同步用的越多,越有可能导致性能问题,也增大了出错的可能。

另外每个处理器都缓存了的变量值的备份,在某些类型的处理器中,即使其它处理器利用内存栅栏(memory barriers)将新值写入了共享内存,因为处理器用的是它自己的备份值,还是会认为helper值为null,导致新建了Helper实例,并使用了这个新实例。(Alpha处理器)

 

5. 可以对32位的原始类型数据变量用双检锁(如int和float)

因为原始类型数据的变量存的就是值本身,所以对它赋值就直接改了变量内容。但是64位的原始类型数据变量,如long和double,就无法保证该操作的原子性。

class Foo {     private int cachedHashCode = 0;    public int hashCode() {        int h = cachedHashCode;        if (h == 0)             synchronized(this) {                if (cachedHashCode != 0) return cachedHashCode;                h = computeHashCode();                cachedHashCode = h;            }        return h;    }}

 

6. 利用线程本地存储的双检锁方法

class Foo {    // 如果 perThreadInstance.get() 返回非null的值,说明已经有线程执行过该方法,helper已经被初始化好了    private final ThreadLocal perThreadInstance = new ThreadLocal();    private Helper helper = null;    public Helper getHelper() {        if (perThreadInstance.get() == null) createHelper();        return helper;    }    private final void createHelper() {        synchronized(this) {            if (helper == null)                helper = new Helper();        }        // 任何非空的值都可作为set的参数        perThreadInstance.set(perThreadInstance);    }}

该方法的效率取决于JDK中ThreadLocal的实现。

 

7. 利用volatile的双检锁方法

从JDK5开始,volatile严格地限制了变量的读写顺序,不允许重排。

class Foo {    private volatile Helper helper = null;    public Helper getHelper() {        if (helper == null) {            synchronized(this) {                if (helper == null)                    helper = new Helper();            }        }        return helper;    }}

对不可变对象(Immutable Objects)引用的读写都是原子性的操作。所以如果Helper是不可变对象,如String和Integer,可以不用volatile关键字。

 

8. 总结:

尽量参考使用已有的最佳实践,不要自己去发明。

一门合适的编程语言应该是优雅的。如果发现已有的问题很难用优雅的方式解决,要么换一种语言,要么发出声音,让开发这门语言的人从源头改进该语言。

对于绝大多数只是使用语言的你,不要为了研究语言而研究,应该基于现有的业务问题去研究。多接触各种业务,自然就会多遇到各种问题,自然有机会学得更多。

 

《C# 单例模式整理》

0 0