java锁以及双重检查

双检锁/双重校验锁 双层对空判断困扰了很久。实例

public class Singleton {
    private volatile static Singleton singleton;

  //私有构造函数避免调用
    private Singleton (){}
    public static Singleton getSingleton() {

    // 先判断对象是否创建过
        if (singleton == null) {

        //类对象加锁
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                    //字节码层
                    //JIT CPU 可能对如下指令进行重排序
                    // 1.分配空间
                    // 2.初始化
                    // 3.引用赋值
                    如果指令重排后指令如下:
                    //1.分配空间
                    //3.引用赋值 如果在当前指令执行完后,有其他线程获取到实例,将拿到未初始化的实例;
                    //2. 初始化
                }
            }
        }
        return singleton;
    }
}


解释:当A与B同时调用getSingleton时,判断第一个if都为空,这时A拿到锁,进行第二层if判断,条件成立new了一个对象;

B在外层等待,A创建完成,释放锁,B拿到锁,进行第二层if判断,条件不成立,结束释放锁。C调用getSingleton时第一层判断不成立,直接拿到singleton对象返回,避免进入锁,减少性能开销。

进一步理解:其中两次判空,第一次判空是,减少多线程情况下,进入同步代码块的次数,第二次判空,是防止多线程(A,B两种线程的情况下A,B 同时调用了 getSingleton 方法,都同时,进入到第一层if(singleton==null){} 内,竞态条件下,如果A 拿到了对象锁,进入到同步代码块,B阻塞等待,等待创建了实例对象,释放了锁后,B进入同步代码块,但是此时第二层if(singleton==null){} 判断,singleton 不为空,直接返回singleton);

总结:其中有两次判断是否为空的语句,第一次是为了提高效率,避免每次都要执行同步代码块,第二次判空,是为了避免多线程带来的不安全,当两个线程同时对第一个判断为空时,均会先后进入同步代码块,此时,若没有第二个判空条件,则会引来创建多个实例。

volite 关键字:

采用volatile关键字修饰很有必要

这句代码事实上是分三步:

singleton = new Singleton();

  1. 为singleton 分配内存空间
  2. 初始化singleton 
  3. 将singleton 指向分配的内存地址
    但是,jvm会有指令重排的特性,执行顺序有可能改变,不是按照123的顺序,可能是132,这样就会导致一个线程获得没有初始化的实例
    如:t1执行了13,此时t2调用了getInstance()后发现singleton 已经不为空了,因此返回singleton ,但是这时singleton 还没有被初始化
    volatile就可以禁止jvm的指令重排,保证在多线程环境下也能正常运行

想进一步了解:推荐

深入理解Java并发之synchronized实现原理

https://blog.csdn.net/javazejian/article/details/72828483

参考:

https://blog.csdn.net/jlnu0812407/article/details/102836187

https://blog.csdn.net/faye_1008/article/details/90296173

猜你喜欢

转载自blog.csdn.net/JHON07/article/details/103048352