以下摘自某教程:
有序性指的是程序按照代码的先后顺序执行。而编译器为了优化性能,有时候会改变程序中语句的先后顺序。
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 操作应该是:
但是实际上优化后的执行路径却是这样的:
假设线程 A 先执行 getInstance()
方法,当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上;如果此时线程 B 也执行 getInstance()
方法,那么线程 B 在执行第一个判断时会发现 instance != null
,所以直接返回 instance ,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。
问题:线程 A 在 new 之前获取了锁,为啥线程 B 还可以访问?
查资料有人说经过这两步 1.分配一块内存 M ; 2. 将 M 的地址赋值给 instance 变量;
后就会释放锁,不知道对不对
1
strayerxx 2023-05-09 16:17:49 +08:00 1
B 又没进入 synchronized 不需要获取锁,为什么不可以访问
|
2
songche OP @strayerxx 我理解的是 这个 instance 加了 synchronized 锁,那其他线程 B 不就不能访问了嘛。
参考的:synchronized 通过当前线程持有对象锁,从而拥有访问权限,而其他没有持有当前对象锁的线程无法拥有访问权限,保证在同一时刻,只有一个线程可以执行某个方法或者某个代码块,从而保证线程安全。 |
3
strayerxx 2023-05-09 16:31:43 +08:00
@songche 如果是这样随便在一个地方加锁,其他地方都不能访问了,那设计 JUC 的那些大神为什么一门心思的将锁细化,那直接把 synchronized 加到方法上连 double check 都不需要了
|
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
|
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; } } |
8
jtwor 2023-05-09 17:03:01 +08:00
"instance != null ,所以直接返回 instance ,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常"
不懂 java ,有一个疑惑这里 instance 都不为空了怎么可能空引用。 |
10
leonshaw 2023-05-09 17:08:41 +08:00
op 没搞清锁是做什么的,加锁是阻止另一个线程加锁,不是阻止所有对对象的访问。同步操作一般需要双方配合,包括 volatile 也是隐含了读写配对。
|
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 行 |
12
yule111222 2023-05-09 17:14:09 +08:00 1
@jtwor 去看 6 楼的链接吧,说得很清楚。引用赋值和对象初始化是 2 条机器指令,再当前的 JMM 模型下对这 2 条指令做重排序是完全允许的,也就是可以在没有完成构建初始化的情况就给引用赋值了。所以线程 B 可能会拿到尚未初始化完成的对象,这个时候使用这个对象是非常危险的
|
13
jtwor 2023-05-09 17:24:33 +08:00
@yule111222 原来如此,谢谢大佬。我是写.net 的,感觉就是内存屏障问题,主要我们这边的 lock 锁和 volatile 都会处理。 [也就是可以在没有完成构建初始化的情况就给引用赋值了] 这种情况真没听过
|
14
oldshensheep 2023-05-09 17:24:54 +08:00
6 楼的链接里有很多有用的东西,你那个文章说的是对的,一楼解释是对的。
虽然 instance!=null ,但是 instance 并没有初始化,仅仅是分配了内存。 |
15
gaifanking 2023-05-09 17:27:30 +08:00
@yule111222 请教下这个未初始化的对象使用的适合抛的什么异常呢?应该不是空指针吧,指针毕竟赋值了
|
16
yule111222 2023-05-09 17:31:46 +08:00
@gaifanking 使用这个对象里面的属性可能会空指针,因为还没有初始化。
|