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

单例模式 双检测问题请教

  •  
  •   followyourheart · 2022-04-11 09:29:02 +08:00 · 3458 次点击
    这是一个创建于 956 天前的主题,其中的信息可能已经有所发展或是发生改变。
    public class Singleton {
    
        private volatile static Singleton uniqueInstance;
        
        private Singleton(){};
        
        public static Singleton getInstance(){
            if(uniqueInstance == null){
                synchronized(Singleton.class){
                    if(uniqueInstance == null){
                        uniqueInstance = new Singleton();
                    }
                }
            }
            return uniqueInstance;
        }
    }
    

    uniqueInstance = new Singleton() 这行代码虽然会发生指令重排序, 但 synchronized 代码块已经加锁了,每次只有一个线程进入代码块,为啥还要加 volatile ?

    31 条回复    2022-12-03 18:39:08 +08:00
    hay313955795
        1
    hay313955795  
       2022-04-11 09:35:55 +08:00
    加了 synchronized 不代表一定只有一个线程进到了方法里面..之前有个视频里有讲过..

    链接: https://pan.baidu.com/s/1bb-6itr18pnILc6yZCdvyQ 提取码: fbpy 复制这段内容后打开百度网盘手机 App ,操作更方便哦
    jwh199588
        2
    jwh199588  
       2022-04-11 09:38:10 +08:00
    防止重排序的问题,uniqueInstance = new Singleton();这里存在一个排序的问题
    Kaiv2
        3
    Kaiv2  
       2022-04-11 09:40:07 +08:00
    https://cdmana.com/2022/03/202203090332315266.html

    这里讲的比较清楚
    double-checked locking
    xuyang2
        4
    xuyang2  
       2022-04-11 09:40:12 +08:00
    都什么年代了,还用 `static Singleton instance` ...
    Suddoo
        5
    Suddoo  
       2022-04-11 09:41:23 +08:00 via iPhone
    用 enum 吧,单例模式已经没意义了,甚至以前的那些设计模式都没有意义了,Java 语言是在不断演进的
    wolfie
        6
    wolfie  
       2022-04-11 09:42:55 +08:00
    其他线程即时可见
    qgs
        7
    qgs  
       2022-04-11 09:45:24 +08:00
    idea 里面在第一个 if 上面,按两次 ctrl + f1 ,会出现解释

    Double-checked locking
    Inspection info: Reports double-checked locking.
    Double-checked locking tries to initialize a field on demand and in a thread-safe manner while avoiding the cost of synchronization. Unfortunately it is not thread-safe when used on a field that is not declared volatile. When using Java 1.4 or earlier, double-checked locking doesn't work even with volatile fields. Read the article linked above for the detailed explanation of the problem.
    Example of an incorrect double-checked locking:
    class Foo {
    private Helper helper = null;
    public Helper getHelper() {
    if (helper == null)
    synchronized(this) {
    if (helper == null) helper = new Helper();
    }
    return helper;
    }
    }
    // other functions and members...
    }
    MapHacker
        8
    MapHacker  
       2022-04-11 09:49:13 +08:00
    加锁之前的那个 if 会出问题,直接等于 false 拿到一个未初始化完毕的 instance
    pennai
        9
    pennai  
       2022-04-11 09:50:36 +08:00
    如果不加 volatile ,取得锁进行初始化的线程对变量的更新操作不一定能及时地被其他线程感知,其他线程有可能还是会判断 uniqueInstance == null 为 true ,volatile 是保证了可见性,invalidate 了其他线程工作内存的变量副本。

    "普通变量与 volatile 变量的区别是,volatile 的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此我们可以说 volatile 保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。"
    出自深入理解 java 虚拟机第三版
    mxalbert1996
        10
    mxalbert1996  
       2022-04-11 10:03:07 +08:00 via Android
    不加 volatile 也不会有错但是加了 volatile 可以在其他线程已经生成实例时直接返回,避免同步,所以性能会更好。
    liudaolunhuibl
        11
    liudaolunhuibl  
       2022-04-11 10:03:09 +08:00
    这里加 volatile 不是为了防止指令重排是为了线程之间的可见性
    XieQing0428
        12
    XieQing0428  
       2022-04-11 10:20:29 +08:00
    @Suddoo 啊?能问问原因吗,我平常还挺经常用的
    Leviathann
        13
    Leviathann  
       2022-04-11 10:27:29 +08:00
    两线程,一个判断完第一个 null ,另一个已经拿完锁创建并返回了
    那不加 volatile 第一个线程接下来进到同步块里的那个判空不是就无效了吗,直接从工作区取的,和外层的判空完全一致了
    fantastM
        14
    fantastM  
       2022-04-11 10:33:50 +08:00
    @Suddoo #5 这段 double-checked 单例模式是懒加载的,和 enum 还是有点区别的
    praxis
        15
    praxis  
       2022-04-11 10:35:25 +08:00   ❤️ 1
    uniqueInstance = new Singleton(); 会被编译成三条计算机指令
    1 、为 uniqueInstance 分配一个内存地址 A
    2 、在内存地址 A 上初始化 uniqueInstance 实例
    3 、把内存 A 的地址赋值给 uniqueInstance 变量
    如果不禁止指令重排 可能导致 顺序变为 1 、3 、2
    这样的话 当一个线程执行到 1 、3 得到的是一个未初始化的 uniqueInstance 而另一个线程执行到第一个 if 就会返回一个未初始化的 uniqueInstance
    golangLover
        16
    golangLover  
       2022-04-11 10:46:12 +08:00 via Android
    @xuyang2 @Suddoo 为什么这么说
    fkdog
        17
    fkdog  
       2022-04-11 10:56:47 +08:00   ❤️ 1
    两个线程,假如 uniqueInstance 非 volatile 。
    0:00 ,A 刚进入方法内,uniqueInstance 此时应该是从内存里 copy 了到线程的工作内存里。

    0:01 ,B 刚离开同步块,B 完成 uniqueInstance 的初始化,将本地工作线程里的 uniqueInstance 同步回主内存。
    由于 uniqueInstance 非 volatile ,A 线程无法感知 B 线程种的同步变化,因此 A 会继续走剩余的逻辑进入同步块。由于同步块里会将 uniqueInstance 变量进行同步,同步完会发现 uniqueInstance 非空,因此需要重新判断一次非空来保证 uniqueInstance 不会被重复初始化。

    如果 uniqueInstance 是 volatile ,那么 A 可以感知到 uniqueInstance 的变化,从而避免进入同步块降低吞吐。

    总结:
    1. 代码进入同步块以后,uniqueInstance 可能已经发生变化,多加一层 null 判断是为了防止重复初始化。
    2. 加 volatile 是为了防止代码进入不必要的同步块,提高性能。
    TWorldIsNButThis
        18
    TWorldIsNButThis  
       2022-04-11 11:22:15 +08:00   ❤️ 1
    @XieQing0428
    有很多 design pattern 是为了给 java 、C++这帮 oop 的语言当年没有函数这个抽象擦屁股
    搜 design pattern functional programming
    当然 fp 语言有自己的 pattern ,比如 monad
    另外随着 java pattern matching 能力的到来,Visitor Pattern Considered Pointless (这个是 oracle java 团队成员的一篇博文)
    Suddoo
        19
    Suddoo  
       2022-04-11 12:13:10 +08:00 via iPhone
    2022 年了,还是会有傻逼面试官问设计模式,就单例讲出一堆“底层”原理。时代变了,明明有了自动挡汽车,非要开手动挡
    Suddoo
        20
    Suddoo  
       2022-04-11 12:15:28 +08:00 via iPhone
    @TWorldIsNButThis 是这样的,因为当时 Java 语言还不完善,才弄出这么多补救措施,但现在,很多设计模式已经没有存在的必要了
    BQsummer
        21
    BQsummer  
       2022-04-11 13:04:38 +08:00 via Android
    @Suddoo 自动挡汽车指的是什么
    git00ll
        22
    git00ll  
       2022-04-11 13:51:56 +08:00
    a 线程使用赋值语句赋值后,b 线程可能看不到
    crackhopper
        23
    crackhopper  
       2022-04-11 13:57:28 +08:00
    每次看到 volatile 就头疼。C++和 Java 的 volatile 还不一样。
    liudaolunhuibl
        24
    liudaolunhuibl  
       2022-04-11 13:58:51 +08:00
    @BQsummer 用枚举吧,话说这种手写单例模式的还怎么见过,一般你需要某个类单例就把它交给他 spring 就好了
    chengyiqun
        25
    chengyiqun  
       2022-04-11 14:10:37 +08:00
    推荐用静态内部类的写法代替这种写法, 或者用枚举, 最好用 spring 直接管理.
    ligiggy
        26
    ligiggy  
       2022-04-11 14:24:05 +08:00
    @crackhopper 哈哈哈,太经典了
    quicksand
        27
    quicksand  
       2022-04-11 14:39:50 +08:00
    zhady009
        28
    zhady009  
       2022-04-11 14:58:19 +08:00
    要延迟加载用内部静态类的方式就好了 这种写法不知道多少年前的
    如果不用延迟加载用 static final 或者直接 enum
    MakHoCheung
        29
    MakHoCheung  
       2022-04-11 15:28:10 +08:00
    同意 25 楼,与其深究这种容易出问题需要了解底层的方式,还不如去掌握没问题的方式
    Red998
        30
    Red998  
       2022-04-15 16:45:59 +08:00
    是对象初始化有几个步骤、申请内存--初始化--赋值--返回内存引用地址 。多线程情况下可能会存在 获取半初始化对象 。所有才需要 volatile 防止指令冲排序 至于为什么 cpu 指令会这样。个人觉得效率高吧。无论怎么样执行最后都是返回一个对象引用地址。 不过话又说回来 我真没看到现在有这么用 。一般的单例用的多也没啥大问题。非要完美一点 我觉得那个枚举方式就很好
    goalidea
        31
    goalidea  
       2022-12-03 18:39:08 +08:00
    没有必要搞这么细,这也就面试中可能会问一问,工作中都是懒汉,不然就内部类实现
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5254 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 05:58 · PVG 13:58 · LAX 21:58 · JFK 00:58
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.