双重锁单例

双重锁单例模式,代码如下(代码是从我的好同事这里直接拷贝的)


public class Singleton {  
    private volatile static Singleton instance;  

    private Singleton () {
    }  

    public static Singleton getSingleton() {  
        if (instance == null) {  
            synchronized (Singleton.class) {  
                if (instance == null) {  
                    instance = new Singleton();  
                }  
            }  
        }  
        return instance;  
    }  
}

看同事的总结,有几个问题说一下:
1.为什么用volatile修饰
2.为什么要加synchronized
3.为什么要用Singleton.class作为加锁对象
4.为什么两个null判断

第一个问题:为什么用volatile修饰
volatile就扯到了指令重排序,哪一句呢 就是new Singletone()这一句,这一句不是一个原子操作。
java语言这里是一行代码,但是实际编译出来的指令不止一行。最起码有三个操作。(参考

  1. 分配对象内存空间
  2. 初始化对象
  3. 设置instance指向刚刚分配的内存地址, 此时instance != null (重点)

这里主要问题就是假如没有volatile修饰, 两个线程A,B,A走到了第一个if判断 A判断instance为空,A拿到了锁,开始执行new操作,因为没有volatile修饰,那么可能指令重排序按照1-3-2的顺序执行操作。
3操作执行完instance就不为空了。恰好这时候线程B也走到了第一个if判断,它这时候判断instance不为空。就直接返回instance,实际上呢,线程B拿到的instance是一个初始化一半的对象。

这里有个非常奇怪的问题,明明已经用synchronized修饰了啊,但是实际上synchronized只是对同步代码块进行加锁,别的线程仍然可以访问同步代码块之外的instance变量。而且synchronized也没有对第一个if判断进行加锁。
volatile如何解决这个骚操作的呢,通过它的禁止指令重排序,要知道指令重排序不是指令乱排序,它的目的是在确保程序可以得到正确结果的基础上对指令进行重排序来优化程序的执行效率,如何得到正确结果,简单说就是指令间有逻辑依赖关系的不能重排,没有依赖关系的可以重排,这里1跟2,3都有依赖关系,2,3之间没有依赖关系,也就是说再怎么重排也不会排个2-1-3或者3-1-2之类的出来。禁止指令重排序的原理是内存屏障,也就是lock关键字,volatile修饰的变量赋值的时候会有一个lock前缀修饰的指令,这个lock前缀的作用说白了就是lock前缀修饰的指令一定是最后执行。也就是3执行的时候1,2肯定是执行完了。这样一来,线程B走到第一if判断时instance变量要么为空要么就是已经初始化完全的变量,不会拿到一个初始化一半的变量。

第二个问题:为什么要用synchronized修饰
假设没有synchronized修饰,我们来看一下,没有synchronized修饰的化代码就变成了


    public static Singleton getSingleton() {  
        if (instance == null) {  
                instance = new Singleton();  
            }  
        }  
        return instance;  
    }  

这个非常容易理解,这不就变成懒汉模式了吗,不是线程安全的了。

第三个问题:为什么要用Singleton.class作为加锁对象
这个问题就扯到了类锁和对象锁。
两个区别是什么呢:
类锁就是synchronized修饰static方法或者代码块用类.class作为加锁对象。
对象锁就是synchronized修饰普通方法或者代码块拿一个实例变量作为加锁对象。

为什么用类锁是因为类锁是唯一的。
怎么个唯一法呢:
无论是static修饰的方法是静态方法还是以类.class作为加锁对象,实际上都是以类对象来加锁,无论这个类如何创建,类对象都只有一个。
有一篇文章对类锁和对象锁讲解的非常生动有趣,地址在这里

第四个问题:为什么两个null判断
这个问题也很简单,两个线程A,B,同时走到了第一个if判断,线程A拿到了锁,线程B被阻塞,然后线程A执行new Singleton()操作,操作完毕后A释放锁,线程B拿到锁,这时候B如果没有第二次判断,就会认为instance还是个空对象,会再次执行new Singleton()操作,所以加了第二个空判断,防止这种问题发生。

posted @ 2021-02-07 19:34  cfdroid  阅读(237)  评论(0编辑  收藏  举报