Doublechecked locking解析

参照:http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

概念:DCL是为了避免在单例在方法中使用synchronized关键字,从而减少系统支出

来一个个解析参照里的例子:

/**
 * Copyright 2012 Garin Zhang
 */
package com.garinzhang.singleton;

/**
 * @author GarinZhang
 *
 */
public class Singleton1SingleThread {
    private Singleton1SingleThread helper = null;
    
    /**
     * 这是最简单的单例,但是如果使用在多线程的时候,
     * 就会出现很多问题,最明显的:多个helper对象将被赋值
     */
    public Singleton1SingleThread getHelper() {
        if(helper == null) {
            helper = new Singleton1SingleThread();
        }
        return helper;
    }
}
/**
 * Copyright 2012 Garin Zhang
 */
package com.garinzhang.singleton;

/**
 * @author GarinZhang
 *
 */
public class Singleton2MultiThread {
    private Singleton2MultiThread helper = null;
    
    /**
     * 与前一个版本唯一不同之处,此处方法添加了synchronized,
     * 这样可以在多线程中执行,但是每次方法执行的时候都必须同步,消耗太大,
     * 接下来看看DCL(Double-checked Locking),
     * 看能不能在helper被赋值之后避免synchronized
     */
    public synchronized Singleton2MultiThread getHelper() {
        if(helper == null) {
            helper = new Singleton2MultiThread();
        }
        return helper;
    }
}
/**
 * Copyright 2012 Garin Zhang
 */
package com.garinzhang.singleton;

/**
 * @author GarinZhang
 *
 */
public class Singleton3DCL1 {
    private Singleton3DCL1 helper = null;
    
    /**
     * 这里就是所谓的DoubleCheckLocking机制
     * 但是这段代码在一些优化的编译器或者内存共享的多处理器上不工作
     * 最根本原因在于:helper = new Singleton3DCL1();这句话
     * 这句话实际在是执行了两个动作:
     * 1. helper = new Singleton3DCL1(); 为这个实例对象分存储空间,将这个引用赋值给helper
     * 2. new Singleton3DCL1(); 初始化构造器
     * 当以上第一个动作执行完之后,有另外一个线程访问getHelper时,就会出现helper不为空,
     * 但是实际上helper引用的值又不是一个实例华的Singleton3DCL1
     */
    public Singleton3DCL1 getHelper() {
        if(helper == null) {
            synchronized(this) {
                if(helper == null) {
                    helper = new Singleton3DCL1();
                }
            }
        }
        return helper;
    }
}
/**
 * Copyright 2012 Garin Zhang
 */
package com.garinzhang.singleton;

/**
 * @author GarinZhang
 *
 */
public class Singleton4DCL2 {
    private Singleton4DCL2 helper = null;
    
    /**
     * 这里就是所谓的DoubleCheckLocking机制
     * 但是这段代码在一些优化的编译器或者内存共享的多处理器上不工作
     * 最根本原因在于:helper = new Singleton3DCL1();这句话
     * 这句话实际在是执行了两个动作:
     * 1. helper = new Singleton3DCL1(); 为这个实例对象分存储空间,将这个引用赋值给helper
     * 2. new Singleton3DCL1(); 初始化构造器
     * 当以上第一个动作执行完之后,有另外一个线程访问getHelper时,就会出现helper不为空,
     * 但是实际上helper引用的值又不是一个实例华的Singleton3DCL1
     * 
     * 以上问题解决方法,即保证不让两个动作分开操作,如下从代码层面确实保证了h在赋给helper时一定是已经初始化过的,
     * 最里面的synchronized相当于是一个memory barrier,即为了保证以上的步骤1,2执行顺序,
     * 但是实际情况还是不可行,因为这取决于处理器执行的操作
     */
    public Singleton4DCL2 getHelper() {
        if(helper == null) {
            Singleton4DCL2 h;
            synchronized(this) {
                h = helper;
                if(h == null) {
                    synchronized(this) {
                        h = new Singleton4DCL2();
                    } // release inner synchronization lock
                }
                helper = h;
            }
        }
        return helper;
    }
}
/**
 * Copyright 2012 Garin Zhang
 */
package com.garinzhang.singleton;

/**
 * @author GarinZhang
 *
 */
public class Singleton5Static {
    /**
     * 直接用static标识变量,是一种最简单的方式
     * 但是所有对此对象进行更改都会被其他调用者看到
     */
    static Singleton5Static helper = new Singleton5Static();
}
/**
 * Copyright 2012 Garin Zhang
 */
package com.garinzhang.singleton;

/**
 * @author GarinZhang
 *
 */
public class Singleton6Primitive {
    /**
     * 这种方式只对原始值(primitive values)有用,即对int或float有效
     * 但是这种情况只在32位上有效,对64位无效,因为64位系统不能保证其操作的原子性
     * 实际上这种情况下,synchronized已经没有任何意义了
     */
    private int cachedHashCode = 0;
    public int hashCode() {
        int h = cachedHashCode;
        if(h == 0) {
            synchronized(this) {
                if(cachedHashCode != 0) {
                    h = 1;
                }
                cachedHashCode = h;
            }
        }
        return h;
    }
}

解决方案:

/**
 * Copyright 2012 Garin Zhang
 */
package com.garinzhang.singleton;

/**
 * @author GarinZhang
 *
 */
public class Singleton7ThreadLocal {
    /**
     * 使用ThreadLocal来检测是否被实例化,TL存储实现DCL,这样就避免了前面遇到的这些问题(lazy initialization)
     * ThreadLocal取决于JDK的实现,在JDK1.2里非常慢,但是在1.3以后有了很大的提高
* 参考:http://blog.csdn.net/lufeng20/article/details/24314381
*/ private final ThreadLocal tlInstance = new ThreadLocal(); private Singleton7ThreadLocal helper = null; public Singleton7ThreadLocal getHelper() { if(tlInstance.get() == null) { createHelper(); } return helper; } private final void createHelper() { synchronized(this) { if(helper == null) { helper = new Singleton7ThreadLocal(); } } tlInstance.set(tlInstance); } }
/**
 * Copyright 2012 Garin Zhang
 */
package com.garinzhang.singleton;

/**
 * @author GarinZhang
 *
 */
public class Singleton8Volatile {
    /**
     * 使用JDK1.5的新特性volatile实现单例
     * volatile能够保证实例化的的顺序
     * 也就是先实例化Singleton8Volatile的构造器,然后再赋值给helper
     */
    private volatile Singleton8Volatile helper = null;
    public Singleton8Volatile getHelper() {
        if(helper == null) {
            synchronized(this) {
                if(helper == null) {
                    helper = new Singleton8Volatile();
                }
            }
        }
        return helper;
    }
}

这里有一个比较清晰的关于MemoryBarrier的描述:http://bbs.chinaunix.net/thread-3763450-1-1.html

问题在于 pInst = new Singleton(); 这一句,它包含了 内存分配 和 构造 两步
也就是实际执行时可能是
a. 分配内存,将内存地址赋给 pInst
b. 在上一步分配的内存上调用构造函数

如果在a执行完毕,b未执行完毕期间,有另一个线程调用 GetInstance(),那么就可能获得了一个没有构造完毕的对象地址。

如果能严格按照
temp = new Singleton()
pInst = temp
的顺序来执行(当然也要假定pInst=temp是个原子操作),似乎可以解决这个问题。但 IA64 和 x86/x64 不同,它会乱序执行代码。也就是即使编译器给出了顺序的汇编指令也可能被它智能的搞乱
于是在这两句中需要加一句 MemoryBarrier
这一句MemoryBarrier的作用是让CPU不会将 MemoryBarrier后面的内存存取 发生在 MemoryBarrier前面的内存存取 之前。

路慢慢其休远羲,吾将上下而求所
原文地址:https://www.cnblogs.com/garinzhang/p/2999331.html