并发可见性问题
可见性:线程对主内存的修改可以及时的被其他线程观察到
static boolean stop = false;
new Thread(() -> {
// 线程1 执行
System.out.println("线程1 is running...");
// 线程1 等待线程2将 stop状态改为true,然后停止执行
while (!stop) ;
// 然后输出
System.out.println("线程1 is terminated.");
}).start();
Thread.sleep(10);
new Thread(() -> {
// 线程2 执行
System.out.println("线程2 is running...");
// 线程2 将状态改为true,此时线程1应该停止执行
stop = true;
// 线程2 执行
System.out.println("线程2 B is terminated.");
}).start();
执行结果:
线程1 is running...
线程2 is running...
线程2 is terminated.
// 线程1 仍然在执行,虽然stop已经变成了true
上面的程序中,线程2的虽然将共享变量stop设置为了true,但是线程1仍然继续执行,确实存在不可见性的问题。
线程之间不可见
线程之间的不可见是有Java内存模型决定的:
- Java所有变量都存储在主内存中
- 每个线程都有自己独立的工作空间
- 每个线程的工作空间中,存储了使用到的共享变量的副本
- 线程对共享变量的操作,都会在自己的工作内存中进行,不能直接在主内存中读写
- 不同线程之间,无法访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成
volatile保证可见性
上述程序如果想要保证可见性,只需要给变量stop
添加关键字volatile
即可,volatile的意思是可变的、易变的,指代此变量会发生变化:
static volatile boolean stop = false;
volatile 保证可见性是通过store与load指令完成的;也就是对volatile变量执行写操作时,会在写操作后加入一条store指令,即强迫线程将最新的值刷新到主内存中;而在读操作时,会加入一条load指令,即强迫从主内存中读入变量的值。
volatile不保证volatile变量的原子性
synchronized保证可见性
注意,synchronized也可以保证可见性: 在JMM中,synchronized规定,线程在加锁时,先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁
。
CPU MESI
CPU缓存一致性协议。
指令重排序
CPU在执行指令前,为了提高执行效率,会进行指令重排序,而volatile关键字将会禁止指令重排序。
单例模式-双重检查锁
public class ST {
public static /*volatile*/ ST INSTANCE;
public static ST getInstance() {
if (INSTANCE == null) {
synchronized (ST.class) {
if (INSTANCE == null) {
INSTANCE = new ST();
}
}
}
return INSTANCE;
}
}
上述INSTANCE变量没有增加 volatile,在超高并发下有可能因指令重排序发生问题。以Object o = new Object();
为例,转换的指令如下:
NEW java/lang/Object # 申请内存
DUP
INVOKESPECIAL java/lang/Object.<init> ()V # 初始化成员变量
ASTORE 1 # 赋值给引用
由于现代CPU大多采用流水线式的执行方式,JVM也有类似的指令重排序,所以指令执行的顺序并不一定按着从上至下的顺序执行,所以上述指令执行可能变为:
申请内存
赋值给引用 #此时不是null
初始化对象
所以判断InSTANCE == null
位false,retrun了一个申请内存后给的默认值
,而程序期待的是一个初始化完成的Object对象。
读屏障、写屏障
CPU支持两种原语,在这两种原语中的所有指令不可进行重排序:
- loadfence
- storefence