# 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不会如预想的停下来
    }
}

分析:为什么会出现这种情况

image.png 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 为什么不能保证原子性

  • 因为不能解决指令交错
    • 写屏障仅仅是保证之后的读能够读取到最新的结果,但不能保证读跑到它前面去。
    • 而有序性的保证只是保证了本线程内相关代码不被重排序。

image.png

t1写屏障可以保证写屏障之前的操作同步到主存去,但他不能保障t2线程的读操作在t1写之前。因为它保证不了俩个线程读取的操作顺序,俩个线程的操作顺序是由cpu保证的,谁的时间片用完了就会切换到另一个线程。不能保证线程间的重排序,只能保证同一线程内的重排序,所以不能保证有序性。