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

2023-05-09 16:07:13 +08:00
 songche

以下摘自某教程:

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

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

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 变量; 后就会释放锁,不知道对不对

1900 次点击
所在节点    Java
16 条回复
strayerxx
2023-05-09 16:17:49 +08:00
B 又没进入 synchronized 不需要获取锁,为什么不可以访问
songche
2023-05-09 16:24:38 +08:00
@strayerxx 我理解的是 这个 instance 加了 synchronized 锁,那其他线程 B 不就不能访问了嘛。
参考的:synchronized 通过当前线程持有对象锁,从而拥有访问权限,而其他没有持有当前对象锁的线程无法拥有访问权限,保证在同一时刻,只有一个线程可以执行某个方法或者某个代码块,从而保证线程安全。
strayerxx
2023-05-09 16:31:43 +08:00
@songche 如果是这样随便在一个地方加锁,其他地方都不能访问了,那设计 JUC 的那些大神为什么一门心思的将锁细化,那直接把 synchronized 加到方法上连 double check 都不需要了
skyemin
2023-05-09 16:31:43 +08:00
@songche 锁的是代码块,if (singleton == null)在代码块之外
strayerxx
2023-05-09 16:33:49 +08:00
@songche 可以理解一下单例模式中的懒汉式和 DCL 的区别
jambo
2023-05-09 16:43:46 +08:00
@songche 如果你是 Java 的初学者, 不建议在这里花太多时间; 如果你在研究并发编程部分, 建议花点时间看下 Java 内存模型(jsr133), 特别是 jsr133 faq. 这个例子就是 jsr133 faq 里的: https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#dcl
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
2023-05-09 17:03:01 +08:00
"instance != null ,所以直接返回 instance ,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常"

不懂 java ,有一个疑惑这里 instance 都不为空了怎么可能空引用。
jambo
2023-05-09 17:07:35 +08:00
@jtwor instance 本身不为 null, 是它指向的那个对象的属性没有完成初始化, 访问这些属性的时候可能抛空指针
leonshaw
2023-05-09 17:08:41 +08:00
op 没搞清锁是做什么的,加锁是阻止另一个线程加锁,不是阻止所有对对象的访问。同步操作一般需要双方配合,包括 volatile 也是隐含了读写配对。
gaifanking
2023-05-09 17:09:03 +08:00
标题写的可见性,内容却是说的有序性,这是两个问题。
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
2023-05-09 17:14:09 +08:00
@jtwor 去看 6 楼的链接吧,说得很清楚。引用赋值和对象初始化是 2 条机器指令,再当前的 JMM 模型下对这 2 条指令做重排序是完全允许的,也就是可以在没有完成构建初始化的情况就给引用赋值了。所以线程 B 可能会拿到尚未初始化完成的对象,这个时候使用这个对象是非常危险的
jtwor
2023-05-09 17:24:33 +08:00
@yule111222 原来如此,谢谢大佬。我是写.net 的,感觉就是内存屏障问题,主要我们这边的 lock 锁和 volatile 都会处理。 [也就是可以在没有完成构建初始化的情况就给引用赋值了] 这种情况真没听过
oldshensheep
2023-05-09 17:24:54 +08:00
6 楼的链接里有很多有用的东西,你那个文章说的是对的,一楼解释是对的。
虽然 instance!=null ,但是 instance 并没有初始化,仅仅是分配了内存。
gaifanking
2023-05-09 17:27:30 +08:00
@yule111222 请教下这个未初始化的对象使用的适合抛的什么异常呢?应该不是空指针吧,指针毕竟赋值了
yule111222
2023-05-09 17:31:46 +08:00
@gaifanking 使用这个对象里面的属性可能会空指针,因为还没有初始化。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/938630

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX