V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
lawlietxxl
V2EX  ›  Java

菜菜问一个单例模式加锁的问题

  •  
  •   lawlietxxl · 2016-09-23 17:44:52 +08:00 · 3844 次点击
    这是一个创建于 2765 天前的主题,其中的信息可能已经有所发展或是发生改变。

    来源: http://www.cnblogs.com/coffee/archive/2011/12/05/inside-java-singleton.html

    其中有一个方法

    public static SingletonThree getInstance() {
            if (instance == null) { 
                synchronized (SingletonThree.class) {           // 1
                    SingletonThree temp = instance;             // 2
                    if (temp == null) {
                        synchronized (SingletonThree.class) {   // 3  这里问什么再加一个锁呢?
                            temp = new SingletonThree();        // 4
                        }
                        instance = temp;                        // 5
                    }
                }
            }
            return instance;
        }
    

    请问在 //3 的位置,为什么再加一个锁呢?百思不得其解,求教

    29 条回复    2016-09-27 04:28:04 +08:00
    suikator
        1
    suikator  
       2016-09-23 18:10:04 +08:00
    可能是为了防止指令重排吧,第 4 和第 5 句,说错了请轻喷, 233
    suikator
        2
    suikator  
       2016-09-23 18:10:24 +08:00
    //4 //5 手滑
    jigloo
        3
    jigloo  
       2016-09-23 18:15:47 +08:00   ❤️ 1
    limhiaoing
        4
    limhiaoing  
       2016-09-23 18:16:18 +08:00 via iPhone
    可能多个线程检查到等于 null ,但只允许 new 一次,好像叫 double-check 什么的。
    hactrox
        5
    hactrox  
       2016-09-23 18:17:31 +08:00
    单例模式中这个概念叫“双重锁定”
    解释起来要打很多字 =.= 所以顺手给 lz 搜了个资料:
    http://blog.csdn.net/ouyang_peng/article/details/8885840
    limhiaoing
        6
    limhiaoing  
       2016-09-23 18:17:39 +08:00 via iPhone
    Java 的 memory order 应该是顺序一致性,所以这里不会重排。
    jigloo
        7
    jigloo  
       2016-09-23 18:25:01 +08:00
    @limhiaoing 是的,只有 c++ 编译器有这个问题(内存栅栏)。

    不过如果能用 std::call_once 的话,一行就解决问题了
    http://www.nuonsoft.com/blog/2012/10/21/implementing-a-thread-safe-singleton-with-c11/comment-page-1/
    suikator
        8
    suikator  
       2016-09-23 18:43:33 +08:00
    @hactrox 666 ,很有水平的文章
    suikator
        9
    suikator  
       2016-09-23 18:54:58 +08:00
    @limhiaoing 是我说错了, 5l 说得对, 233
    haozhang
        10
    haozhang  
       2016-09-23 18:59:38 +08:00 via iPhone
    我觉得直接用 offical 的单例注解不就完了....
    lawlietxxl
        11
    lawlietxxl  
    OP
       2016-09-23 20:03:41 +08:00
    @suikator
    @limhiaoing
    @jigloo

    首先感谢各位大大,让我又看到了一些新东西。
    我不明白的地方还是没明白,就是为什么要有第二个锁呢?如果没有第二个锁,是否还会出现乱序写入的问题?当线程 1 没有退出最外层的 synchronized , instance == null 是 true ,线程 2 不是也进不去吗?那第二个锁意义在哪里呀 = =
    anthow
        12
    anthow  
       2016-09-23 20:08:21 +08:00
    double check 是指 2 次判断是否为 null 吧。。。 2 次锁不知道为什么。 个人感觉没必要~
    貌似 1.5 以上内存模型已经改了吧。
    suikator
        13
    suikator  
       2016-09-23 20:26:29 +08:00   ❤️ 1
    @lawlietxxl
    就是为什么要有第二个锁呢 -> 避免无序写入问题
    如果没有第二个锁,是否还会出现乱序写入的问题 -> 会
    有第二个锁,是否还会出现乱序写入的问题 -> 会
    用 volatile 声明是否还会出现乱序写入的问题 -> 会
    当线程 1 没有退出最外层的 synchronized , instance == null 是 true ,线程 2 不是也进不去吗 -> 当线程 1 没有退出最外层的 synchronized , instance 可能不会为 null ,但此时对象初始化不完整,线程 2 中 instance == null 为 false 直接返回初始化不完整的对象,当使用这个对象时,发生未知问题。所以为了避免发生这个问题再加一套锁,但是并不能彻底解决问题。
    正确的方式是啥 -> 用 double check 实现单例简直就是个错误示范,尽量用内部类或者枚举实现单例。
    anexplore
        14
    anexplore  
       2016-09-23 20:28:57 +08:00
    所谓 double check 是指 2 次检查 instance == null
    一般写法
    volatile A instance = null;
    .....
    static A instance() {
    if (instance == null) {
    synchronized(A.class) {
    if (instance == null) {
    instance = new A();
    }
    }
    }
    }
    一般用 static 内部类来实现 Singleton
    zonghua
        15
    zonghua  
       2016-09-23 20:52:55 +08:00
    private volatile static SingleDog SingleDog = null;

    这种用 volatile 的我也不太懂
    caixiexin
        16
    caixiexin  
       2016-09-23 20:58:17 +08:00 via Android
    @anexplore 更好的方式是用 enum 来实现单例。
    lawlietxxl
        17
    lawlietxxl  
    OP
       2016-09-23 21:38:41 +08:00
    @suikator
    首先感谢你的时间和回复!!的确用内部类是一个很好的实现单例的方式。( enum 的我还不知道,我查查)

    然后。。。我还是不明白。。。如下图,我把第二个锁去掉了,我是在 instance=temp 进行的赋值,那么会有乱序写入吗?如果没有乱序写入的话,那么 instance 也就不会被提前指向一个半成品了呀。所以第二个锁岂不是没有意义。。求教

    public static SingletonThree getInstance() {
    if (instance == null) {
    synchronized (SingletonThree.class) { // 1
    SingletonThree temp = instance; // 2
    if (temp == null) {
    temp = new SingletonThree(); // 4
    instance = temp; // 5
    }
    }
    }
    return instance;
    }
    nifury
        18
    nifury  
       2016-09-23 21:51:08 +08:00   ❤️ 1
    @lawlietxxl 嗯……你这么写我一时想不出是否有问题
    然而单例的话为何不让 classloader 做呢?
    直接 static SingletonThree s = new SingletonThree ();
    suikator
        19
    suikator  
       2016-09-23 22:12:25 +08:00   ❤️ 1
    @lawlietxxl //4 //5 可能被 jit 直接 inline ,变成 instance = new SingletonThree() 这样就又出现了乱序写入问题。哈哈,这逼我装不下去了,已经超出我知识范围了。
    lawlietxxl
        20
    lawlietxxl  
    OP
       2016-09-23 23:01:02 +08:00
    @suikator 似乎是这么一回事!厉害了 我的哥
    pubby
        21
    pubby  
       2016-09-24 02:04:27 +08:00
    @hactrox
    @suikator

    好奇,文中说第二个同步块的问题是 可能 //5 会被优化到 // 4 的同步块内

    但是改成这样呢

    public static SingletonThree getInstance() {
    if (instance == null) {
    synchronized (SingletonThree.class) { // 1
    SingletonThree temp = instance; // 2
    if (temp == null) {
    synchronized (SingletonThree.class) { // 3
    temp = new SingletonThree(); // 4
    }
    synchronized ( OMGOther.class ) {
    instance = temp; // 5
    }
    }
    }
    }
    return instance;
    }

    不会两个同步块合并优化吧。

    不懂 java , phper 路过 -_-
    georgema1982
        22
    georgema1982  
       2016-09-24 02:31:45 +08:00
    为什么现在还在传播这种过时的设计模式?一个 java 类不应该知道自己的生命周期。
    caixiexin
        23
    caixiexin  
       2016-09-24 08:21:17 +08:00 via Android
    @georgema1982 这个说法有相关文档参考吗?学习下 。
    lawlietxxl
        24
    lawlietxxl  
    OP
       2016-09-24 08:31:57 +08:00
    @georgema1982 我的锅 😂
    GhostFlying
        25
    GhostFlying  
       2016-09-24 09:04:07 +08:00 via Android
    static inner class , jvm 标准保证单例,又不像 enum 有所限制,为什么不用呢
    wander2008
        26
    wander2008  
       2016-09-24 11:17:06 +08:00 via iPhone
    static 就够了,不用 violat 了吧
    lawlietxxl
        27
    lawlietxxl  
    OP
       2016-09-24 18:00:06 +08:00
    大概是因为了解历史能够帮助明白现实的成因。。。
    kaedea
        28
    kaedea  
       2016-09-24 22:02:17 +08:00
    1. 锁只需要一个,不过锁前锁后都需要判空;
    2. 最简单的方式是“懒汉模式”,则在声明静态成员变量的时候就初始化,这样在加载类的时候就会完成初始化,因此不需要加锁;
    georgema1982
        29
    georgema1982  
       2016-09-27 04:28:04 +08:00   ❤️ 1
    @caixiexin 准确地说这是一种现代 java 模式设计 practice 。如果你有兴趣,有可以搜索到很多表达支持这种 practice 的文章。我来说说为什么我认为对自己生命周期无知的 pojo 是一种更好的设计模式的原因。

    首先来了解一下 java 设计模式的演变。最初 java 程序员确实是使用对自己生命周期无知的 pojo 类的,也可以说这是一种无设计模式的设计模式。一个类调用另一个类的方法往往是 new 一个 instance ,然后调用新 instance 的方法。但是很快有人注意到这种方式在当时硬件条件下的缺陷,即在多线程模式下,多个做同样事情的 instance 会占用更多的内存,于是在那个时期诞生了很多著名(或者说臭名昭注)的设计模式,最出名的莫过于这种用 private constructor 管理自己生命周期以达到 singleton 的设计模式。

    但是现在 java 界又返璞归珍到原先的 pojo 了。为什么?我认为有这些原因

    1. private constructor 其实产生了代码冗余。它们想实现一种模式,但是为了实现它,每个这种模式的类都在一遍又一遍地重复代码

    2. 由于 constructor 是 private 的,在做单元测试时无法 mock 依赖类。显然在该设计模式产生的年代,单元测试并不被重视。但是现在不同了,单元测试是决定设计模式是否合理的最重要标准

    3. 随着注入依赖这一设计模式的产生, pojo 之间的依赖关系变得更加容易管理,从而使得管理自己生命周期的做法变得毫无意义
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3161 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 10:52 · PVG 18:52 · LAX 03:52 · JFK 06:52
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.