目录
一.可见性问题
退不出的循环:
main
线程对
run
变量的修改对于
t
线程不可见,导致了
t
线程无法停止
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();
Thread.sleep(1);
run = false; // 线程t不会如预想的停下来
}
原因:
1.初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。
2.
因为
t
线程要频繁从主内存中读取
run
的值,
JIT
编译器会将
run
的值缓存至自己工作内存中的高速缓存中, 减少对主存中 run
的访问,提高效率

3. 1
秒之后,
main
线程修改了
run
的值,并同步至主存,而
t
是从自己工作内存中的高速缓存中读取这个变量 的值,结果永远是旧值

二.有序性问题
Result是一个对象,有一个属性r1用来保存结果,问可能的结果有几种?
int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程2 执行此方法
public void actor2(Result r) {
num = 2;
ready = true;
}
分析:
(1)线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1
(2)线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1
(3)线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)
(4)线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2
情况4的现象就是发生了指令重排序(是JIT编译器在运行时的优化)。
指令重排的前提是,重排指令不能影响结果,例如:
// 可以重排的例子
int a = 10; // 指令1
int b = 20; // 指令2
System.out.println( a + b );
// 不能重排的例子
int a = 10; // 指令1
int b = a - 5; // 指令2
三.volatile原理
对
volatile
变量的写指令后会加入写屏障
对
volatile
变量的读指令前会加入读屏障
1.如何保证可见性
写屏障(
sfence
)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
而读屏障(
lfence
)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
2.如何保证有序性
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后。
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前。
public void actor2(Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
}
public void actor1(Result r) {
// 读屏障
// ready 是 volatile 读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}

注意:volatile不能解决原子性问题