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

请教大家一个 Java volatile 可见性问题

  •  
  •   songche · 2023-05-09 16:07:13 +08:00 · 2161 次点击
    这是一个创建于 600 天前的主题,其中的信息可能已经有所发展或是发生改变。

    以下摘自某教程:

    编译优化带来的有序性问题

    有序性指的是程序按照代码的先后顺序执行。而编译器为了优化性能,有时候会改变程序中语句的先后顺序。

    Java 中经典的案例就是利用双重检查创建单例对象,其中 volatile 就是保证有序性的。

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

    如果没有 volatile ,我们以为的 new 操作应该是:

    1. 分配一块内存 M ;
    2. 在内存 M 上初始化 Singleton 对象;
    3. 然后 M 的地址赋值给 instance 变量。

    但是实际上优化后的执行路径却是这样的:

    1. 分配一块内存 M ;
    2. 将 M 的地址赋值给 instance 变量;
    3. 最后在内存 M 上初始化 Singleton 对象。

    假设线程 A 先执行 getInstance() 方法,当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上;如果此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 instance != null ,所以直接返回 instance ,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。

    图片

    问题:线程 A 在 new 之前获取了锁,为啥线程 B 还可以访问?

    查资料有人说经过这两步 1.分配一块内存 M ; 2. 将 M 的地址赋值给 instance 变量; 后就会释放锁,不知道对不对

    第 1 条附言  ·  2023-05-09 17:41:35 +08:00
    感谢大家,已解决,对 synchronized 理解有问题
    16 条回复    2023-05-09 17:31:46 +08:00
    strayerxx
        1
    strayerxx  
       2023-05-09 16:17:49 +08:00   ❤️ 1
    B 又没进入 synchronized 不需要获取锁,为什么不可以访问
    songche
        2
    songche  
    OP
       2023-05-09 16:24:38 +08:00
    @strayerxx 我理解的是 这个 instance 加了 synchronized 锁,那其他线程 B 不就不能访问了嘛。
    参考的:synchronized 通过当前线程持有对象锁,从而拥有访问权限,而其他没有持有当前对象锁的线程无法拥有访问权限,保证在同一时刻,只有一个线程可以执行某个方法或者某个代码块,从而保证线程安全。
    strayerxx
        3
    strayerxx  
       2023-05-09 16:31:43 +08:00
    @songche 如果是这样随便在一个地方加锁,其他地方都不能访问了,那设计 JUC 的那些大神为什么一门心思的将锁细化,那直接把 synchronized 加到方法上连 double check 都不需要了
    skyemin
        4
    skyemin  
       2023-05-09 16:31:43 +08:00
    @songche 锁的是代码块,if (singleton == null)在代码块之外
    strayerxx
        5
    strayerxx  
       2023-05-09 16:33:49 +08:00
    @songche 可以理解一下单例模式中的懒汉式和 DCL 的区别
    jambo
        6
    jambo  
       2023-05-09 16:43:46 +08:00   ❤️ 2
    @songche 如果你是 Java 的初学者, 不建议在这里花太多时间; 如果你在研究并发编程部分, 建议花点时间看下 Java 内存模型(jsr133), 特别是 jsr133 faq. 这个例子就是 jsr133 faq 里的: https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#dcl
    xiang0818
        7
    xiang0818  
       2023-05-09 17:01:17 +08:00
    public class Singleton {
    private static Singleton singleton;
    private Singleton() {
    }
    public static Singleton getInstance() {
    if (singleton == null) {. // 不加 volatile ,线程 B 这行代码会有问题,回取到未初始化的数据
    synchronized (Singleton.class) {
    if (singleton == null) {
    singleton = new Singleton(); // A 在这里 ,这里 M 的地址已经给了 singleton ,但是还没有初始化
    }
    }
    }
    return singleton;
    }
    }
    jtwor
        8
    jtwor  
       2023-05-09 17:03:01 +08:00
    "instance != null ,所以直接返回 instance ,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常"

    不懂 java ,有一个疑惑这里 instance 都不为空了怎么可能空引用。
    jambo
        9
    jambo  
       2023-05-09 17:07:35 +08:00
    @jtwor instance 本身不为 null, 是它指向的那个对象的属性没有完成初始化, 访问这些属性的时候可能抛空指针
    leonshaw
        10
    leonshaw  
       2023-05-09 17:08:41 +08:00
    op 没搞清锁是做什么的,加锁是阻止另一个线程加锁,不是阻止所有对对象的访问。同步操作一般需要双方配合,包括 volatile 也是隐含了读写配对。
    gaifanking
        11
    gaifanking  
       2023-05-09 17:09:03 +08:00   ❤️ 1
    标题写的可见性,内容却是说的有序性,这是两个问题。
    volatile 可以阻止重排序,这个没毛病。
    楼主的问题 1 楼已经回答了,这里锁的不是方法而是代码段。如果锁方法根本不需要 double check

    1 if (singleton == null) {
    2 synchronized (Singleton.class) {
    3 if (singleton == null) {
    4 singleton = new Singleton();
    5 }
    6 }
    7 }
    线程 A 在第 4 行执行,不影响线程 B 进入第 1 行
    yule111222
        12
    yule111222  
       2023-05-09 17:14:09 +08:00   ❤️ 1
    @jtwor 去看 6 楼的链接吧,说得很清楚。引用赋值和对象初始化是 2 条机器指令,再当前的 JMM 模型下对这 2 条指令做重排序是完全允许的,也就是可以在没有完成构建初始化的情况就给引用赋值了。所以线程 B 可能会拿到尚未初始化完成的对象,这个时候使用这个对象是非常危险的
    jtwor
        13
    jtwor  
       2023-05-09 17:24:33 +08:00
    @yule111222 原来如此,谢谢大佬。我是写.net 的,感觉就是内存屏障问题,主要我们这边的 lock 锁和 volatile 都会处理。 [也就是可以在没有完成构建初始化的情况就给引用赋值了] 这种情况真没听过
    oldshensheep
        14
    oldshensheep  
       2023-05-09 17:24:54 +08:00
    6 楼的链接里有很多有用的东西,你那个文章说的是对的,一楼解释是对的。
    虽然 instance!=null ,但是 instance 并没有初始化,仅仅是分配了内存。
    gaifanking
        15
    gaifanking  
       2023-05-09 17:27:30 +08:00
    @yule111222 请教下这个未初始化的对象使用的适合抛的什么异常呢?应该不是空指针吧,指针毕竟赋值了
    yule111222
        16
    yule111222  
       2023-05-09 17:31:46 +08:00
    @gaifanking 使用这个对象里面的属性可能会空指针,因为还没有初始化。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2865 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 14:11 · PVG 22:11 · LAX 06:11 · JFK 09:11
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.