# 1 可见性问题
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();
sleep(1);
run = false; // 线程t不会如预想的停下来
}
}
分析:为什么会出现这种情况
1.初始状态,t线程刚刚从主线程中读取了run的值到工作内存。
2.因为t线程要频繁读取run的值,JIT编译器会将run的值缓存至自己的工作内存中的高速缓存中,减少对主存的访问,提高效率
3.1秒之后,main线程修改了run的值,并且同步到主存中,而t线程是从自己的工作内存中的高速缓存中读取这个变量的值,因此永远是旧值。
# 2 volatile关键字
- 介绍:
- 可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取到它的值,线程操作volatile变量都是直接操作主存。
- 作用:
- 保证可见性
- 防止指令重排序
# 3 什么是指令重排序
- 指令重排序指的是JIT编译器、cpu处理器和jmm定义的多级缓存存储,在编译字节码和运行机器指令时,在不影响程序最终执行结果的情况下,会对原语句执行的顺序进行优化,jmm多级缓存会让语句的执行并不一定是按照正确的读写进行操作的
- 执行重排序在多线程情况下可能会出现问题。
# 4 volatile如何保证可见性
- 对volatile变量的写指令后会加入写屏障
- 对volatile变量的读指令前会加入读屏障
# 4.1 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中。
public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
}
在ready=true这个操作后面加入写屏障(写就是代表赋值操作),加入写屏障效果就是写屏障之前的所有改动都会同步到主存中,在volatile变量后加入写屏障不仅仅会将volatile变量同步到主存中,会顺带把volatile以前的修改也会同步到主存中。例如num=2 这个赋值操作完成以后也是直接同步到主存中的。
# 4.2 读屏障 在该屏障之后,对共享变量的读取,加载的是主存中最新数据
- 例子中 num为成员变量没有任何修饰,ready为用volatile修饰的变量
public void actor1(I_Result r) {
// 读屏障
// ready 是 volatile 读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
在读屏障后面所有的读取操作(可以理解为查看变量的值)都会直接从主存中读取,包括读取ready的值,num的值。
# 5 如何保证有序性
# 5.1 如何保证有序性
- 例子中 num为成员变量没有任何修饰,ready为用volatile修饰的变量
public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
}
写屏障保证了在写屏障之前的代码不会进行指令的重排序,不会排写屏障之后。
# 5.2 读屏障 在该屏障之后,对共享变量的读取,加载的是主存中最新数据
public void actor1(I_Result r) {
// 读屏障
// ready 是 volatile 读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
读屏障保证了在读屏障之后的代码不会进行指令的重排序,排到读屏障之前。
# 6 为什么不能保证原子性
- 因为不能解决指令交错
- 写屏障仅仅是保证之后的读能够读取到最新的结果,但不能保证读跑到它前面去。
- 而有序性的保证只是保证了本线程内相关代码不被重排序。
t1写屏障可以保证写屏障之前的操作同步到主存去,但他不能保障t2线程的读操作在t1写之前。因为它保证不了俩个线程读取的操作顺序,俩个线程的操作顺序是由cpu保证的,谁的时间片用完了就会切换到另一个线程。不能保证线程间的重排序,只能保证同一线程内的重排序,所以不能保证有序性。