1.Java内存模型
JMM即Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
JMM体现在以下几个方面
- 原子性-保证指令不会受到线程上下文切换的影响
- 可见性-保证指令不会受cpu缓存的影响
- 有序性-保证指令不会受cpu指令并行优化的影响
退不出的循环
先来看一个现象,main线程对run变量的修改对于t线程不可见,导致了t线程无法停止:
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while(run){
// ....
}
});
t.start();
sleep(1);
log.debug("停止t");
run = false; // 线程t不会如预想的停下来
}
为什么呢?分析一下:
1.初始状态,t线程刚开始从主内存读取了run的值到工作内存。
2.因为t线程要频繁从主内存中读取run的值,JIT编译器会将run的值缓存到自己工作内存中的高速缓存区中,减少对主存中run的访问,提高效率
3.休眠了1s以后,main线程修改了run的值,并同步到主存,但t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
解决方法
volatile(易变关键字)
它可以用来修饰成员变量和静态变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存
volatile static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while(run){
// ....
}
});
t.start();
sleep(1);
log.debug("停止t");
run = false;
}
也可以用synchronized,但是使用synchronized,要创建Monitor,属于比较重量级的操作,打包volatile相对轻量,在解决可见性层面推荐使用volatile
static boolean run = true;
final static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while(true){
synchronized(lock){
if(!run){
break;
}
}
}
});
t.start();
sleep(1);
log.debug("停止t");
synchronized(lock){
run = false;
}
}
2.volatile原理
2.1.如何保证可见性
写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
public void actor2(I_Result r){
num = 2;
ready = true; // ready是volatile赋值带写屏障
//写屏障
}
而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
public void actor1(I_Result r){
//读屏障
// ready是volatile 读取值带读屏障
if(ready){
r.r1 = num + num;
}else {
r.r1 = 1;
}
}
2.2.如何保证有序性
写屏障会确保指令重排时,不会将写屏障之前的代码排在写屏障之后
public void actor2(I_Result r){
num = 2;
ready = true;
}
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
public void actor1(I_Result r){
//读屏障
// ready是volatile 读取值带读屏障
if(ready){
r.r1 = num + num;
}else {
r.r1 = 1;
}
}
但是还是那句话,不能解决指令交错:
- 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
- 而有序性的保证也只是保证了本线程内相关代码不被重排序