Java多线程之间的【可见性】问题和多条指令执行时的【有序性】问题
- 原子性:保证指令不会受到线程上下文切换的影响
- 可见性:保证指令不会受到CPU缓存的影响
- 有序性:保证指令不会受到CPU并行优化的影响
可见性问题(缓存)
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "test")
public class Test {
// volatile static boolean flag = true;
static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (flag){
}
},"t1").start();
Thread.sleep(1000);
log.info("stop");
flag = false;
}
}
主线程对flag变量的修改对于t1线程来说不可见,导致t1线程无法停止。
原因:
- 初始时,t1线程从主线程读取flag到自己的工作内存
- 由于t1线程需要频繁的从主线程中读取flag,JIT即时编译器会将flag的值缓存在t1的工作内存的高速缓存区
- 1秒后,主线程对flag值进行了修改,但是t1线程读取的仍然是自己工作内存中的flag的旧值。
解决办法:使用volatile修饰flag。
volatile可以用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 修饰的变量都是直接操作主存。
有序性问题(指令重排序)
JVM会在不影响正确性的前提下,可以调整语句的执行顺序。
static int i;
static int j;
i = ...;
j = ...;
在执行上述代码时,先执行i或先执行j,对最终的结果没有影响。所以在真正执行时,
既可以是:
i = ...;
j = ...;
也可以是:
j = ...;
i = ...;
这种特性称为指令重排。多线程下的指令重排会影响正确性。
既然如此为什么要有指令重排这项优化呢?
指令重排优化
指令可以再划分成一个个更小的阶段,例如每条指令都可以分为:取指令-指令译码-执行指令-内存访问-数据写回 这五个阶段。
现代CPU支持多级指令流水线。例如支持同时执行取指令-指令译码-执行指令-内存访问-数据写回的处理器可以称为五级指令流水线。这时CPU可以在一个时钟周期内,同时执行五条指令的不同阶段(相当于执行一条完整的指令)。本质上不能缩短单条指令的执行时间,但变相提高了指令的吞吐量。
使用volatile修饰变量,禁用指令重排序。
指令重排禁用(使用volatile)——volatile原理
volatile的底层实现原理是内存屏障,Memory Barrier
- 对volatile变量的读指令前会加入读屏障
- 对volatile变量的写指令后会加入写屏障
volatile保证可见性
写屏障(sfence)保证在该屏障之前的对共享变量的改动,都同步到主存中。
public void actor2(I_Result r){
num = 2;
ready = true; //ready由volatile修饰,带写屏障
//=======================写屏障
}
读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新的数据,而不是缓存的数据
public void actor1(I_Result r){
//================读屏障
if(ready) {
r.r1 = num +num;
}else{
r.r1 = 1;
}
}
volatile保证有序性
写屏障确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
public void actor2(I_Result r){
num = 2;
ready = true; //ready由volatile修饰,带写屏障
//=======================写屏障
//num = 2;不会出现在这里
}
读屏障确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
public void actor1(I_Result r){
//================读屏障
if(ready) {
r.r1 = num +num;
}else{
r.r1 = 1;
}
}
volatile不能解决指令交错问题。
- 写屏障仅仅保证之后的读能够读到最新的结果,但不能保证其他线程的读在写屏障之前读
- 有序性只是保证了本线程内的相关代码不被重排序
double-checked locking问题
双重验证的单例模式:
public final class Singleton {
private Singleton(){};
private static Singleton INSTANCE = null;
public static Singleton getINSTANCE() {
//多线程首次访问时可能会同步,之后的使用没有进入synchronized
if (INSTANCE == null){
synchronized (Singleton.class){
if (INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
以上实现的特点:
- 懒加载实例化
- 首次使用getINSTANCE会使用synchronized加锁,之后使用时无需加锁
- 隐含的关键一点:第一个if使用INSTANCE变量,是在同步代码块之外
在多线程环境下,上述代码是有问题的:

- 17行表示创建Singleton对象,将对象引用入栈
- 20行表示复制一份对象引用
- 21行表示消耗栈中的一份对象引用,调用构造方法
- 24行表示消耗栈中的一份对象引用,赋值给INSTANCE
JVM可能会指令重排,进行优化:先执行24,再执行21.
如果t1线程先执行了24,在还未来得及执行21时,t2线程执行了该方法,0行getstatic拿到了INSTANCE实例,但此时的INSTANCE实例是没有执行构造方法的,所以t2在使用INSTANCE实例时会出现空指针异常。
错误在于:0行getstatic代码在monitor控制之外,可以越过monitor直接读取INSTANCE变量的值。
对INSTANCE使用volatile修饰,禁用指令重排可以解决该错误。
public final class Singleton {
private Singleton(){};
private static volatile Singleton INSTANCE = null;
public static Singleton getINSTANCE() {
//多线程首次访问时可能会同步,之后的使用没有进入synchronized
if (INSTANCE == null){
synchronized (Singleton.class){
if (INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
字节码层面没有发生改变,看不出来volatile指令的效果。
//-------------------getstatic读操作之前,加入INSTANCE变量的读屏障
0 getstatic #7 <memory/Singleton.INSTANCE : Lmemory/Singleton;>
3 ifnonnull 37 (+34)
6 ldc #8 <memory/Singleton>
8 dup
9 astore_0
10 monitorenter
11 getstatic #7 <memory/Singleton.INSTANCE : Lmemory/Singleton;>
14 ifnonnull 27 (+13)
17 new #8 <memory/Singleton>
20 dup
21 invokespecial #13 <memory/Singleton.<init> : ()V>
24 putstatic #7 <memory/Singleton.INSTANCE : Lmemory/Singleton;>
//-------------------putstatic写操作之后,加入写屏障
27 aload_0
28 monitorexit
29 goto 37 (+8)
32 astore_1
33 aload_0
34 monitorexit
35 aload_1
36 athrow
37 getstatic #7 <memory/Singleton.INSTANCE : Lmemory/Singleton;>
40 areturn
21行构造方法不可能重排序到写屏障下面,也就不可能出现空指针异常。
浙公网安备 33010602011771号