终于搞懂双重检验锁实现单例模式了

Talk is cheap. Show me the code.

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

这是业届奉为经典的 “双重检验锁-懒汉单例”,相信熟悉 Java 语法的人都能看懂,通过这段代码,我们实现了一个单例类,单例类的定义如下,

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己的唯一实例。
  3. 单例类必须给所有其他对象提供这一实例。

通过代码可以看出,由于构造函数私有(private),我们获取 Singleton 类的实例对象只能通过 Singleton.getInstance() 的方式,而无法在外界通过 new 或其他方式创建此类的实例对象,并且由于此成员变量被 static 修饰,使得实例对象属于类本身且只有唯一一个。

也许你已经看懂了这段代码的整体结构,但这段经典代码中仍有几点细节值得思考,比如 volatile,synchronized 和两次出现的 if (instance == null)。
我想,这几个关键字并不陌生。
是的没错,这正是因为我们要保证在并发情况下的安全性问题。

先讲 synchroized ,如果没有 synchronized,

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

在多线程情况下,对于 instance == null 的判断会出现同时多个线程识别到 instance 为 null,同时进行多个实例的创建,即使最终只会有一个实例被引用,可这与我们单例模式设计的初衷并不符合,并且造成大量的内存空间浪费,显然很不合理。
那么如果对方法加上 synchronized 的呢?

public class Singleton {
    private volatile static Singleton instance= null;
    
    private Singleton() {};
    
    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

这样保证只有一个线程进入 getInstance() 方法调用,执行判断与返回,是可以成功解决并发的安全性问题的!但是缺点在于锁的粒度太大了,多个线程同时调用 getInstance() 方法时,除一个线程之外,剩下所有线程都会被阻塞。我只是想读取一下呀,看看也不行么?我们更希望如果 instance 对象存在的话,每个线程都可以直接返回实例对象,所以让我们缩小同步代码的范围。

public class Singleton {
    private volatile static Singleton singelton = null;
    
    private Singleton() {};
    
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized(Singleton.class) {
                singleton = new Singleton();
            }
        }
        return singelton;
    }
}

哈哈哈,是不是合理了很多,线程调用 getInstance() 方法时,只有在发现 instance 为 null 的情况下才会获取锁对象,阻塞其他进程,进行对象的实例化操作。如果 instance 不是 null 的话就直接返回啦。
看到这里,有没有忽然间想起点什么,我们代码的名字,双重检验呀有木有!

public static Singleton getInstance() {
    // 第一次检验
    if (instance == null) {
        synchronized(Singleton.class) {
            // 第二次检验
            if (instance == null) {
                instance = new Singleton();
            }
        }
    }
    return instance;
}

第一次检验是在线程执行 getInstance() 方法时,如果不为 null 就直接返回。那么第二次检验如代码所示,位于 synchronized 修饰的代码块中。
这是由于假设此刻 instance 为 null,如果A,B两个线程同时判断 instance == null 成立,那么两个线程都会进行锁资源的争夺,如果 A 获取到锁资源,则 B 进行阻塞,待 A 完成实例化操作释放掉锁资源后,B 被唤醒,而此刻必须重新判断 instance 的状态,否则 B 会依旧认为 instance 为 null,进行实例化操作,创建新的对象,那么便违背了单例模式只有一个实例对象的原则。

到此为止,我们已经搞懂了 synchronized 和 双重检验,只剩下一个小小的疑问,为什么要加 volatile 呢?

private volatile static Singleton singelton = null;

这就不得不提到有关 java 源码编译后指令执行顺序的两个知识点:

  • instance = new Singleton() 在编译后会被分解为 3 个指令。
  • volatile 的功能之一:禁止指令重排序

先说 instance = new Singleton() 会被分解为三个步骤,

  1. memory=allocate(); // 分配内存 相当于c的malloc
  2. ctorInstanc(memory) //初始化对象
  3. instance=memory //设置instance指向刚分配的地址

而 JVM (Java 虚拟机) 可能会对这三个指令进行重排序,将指令顺序重排为 1→3→2。
那么可能出现这样一种情况,A线程正在执行 instance = new Singleton() 中的 3 指令,即分配完内存空间,并将 instance 指向此内存空间,如果此时恰巧有一个 B 线程执行 getInstance() 方法,会判断 instance 不是 null,将 instance 返回,那么就会返回一个未初始化的对象,造成程序错误。
而用 volatile 就可以完美的解决这个问题,因为被 volatile 修饰的 instance 属性,会在操作其前后设置内存屏障(详见volatile原理),达到禁止其相关指令重排序的功能,使得 instance 一定会被初始化,避免了上述问题。

好啦~,以上就是本文的全部内容了,希望读完的你能够有所收获呀!

1)记住 “双重检验锁-懒汉单例” 的写法。
2)明白为什么 synchronized 在方法内使用。
3)理解双重检验中每次检验的意义。
4)搞懂 volatile 的使用原因。

参考资料:
双重检验的单例模式,为什么要用volatile关键字
双重检验锁思考
《Java并发编程之美》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容