前面几篇内容讲解的java并发主要关注的是访问共享变量时,保证临界区代码的原子性。这一章我们进一步深入学习共享变量在多线程间的【可见性】问题与多条指令执行时的【有序性】问题
1、JAVA内存模型(JMM)
JMM 即 Java Memory Model,它从java层面定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。JMM 体现在以下几个方面
-
原子性 - 保证指令不会受到线程上下文切换的影响(前面讲解)
-
可见性 - 保证指令不会受 cpu 缓存的影响
-
有序性 - 保证指令不会受 cpu 指令并行优化的影响
后面开始讲解可见性和有序性:
2.可见性
先看一段代码,尝试运行:
public class CantGoOut {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
// System.out.println("加上我就可以停下来了"); 如果加上这个代码就会停下来
}
});
t.start();
Thread.sleep(1000);
System.out.println("尝试让线程一停下来");
run = false; // 线程t不会如预想的停下来
}
}
为什么呢?分析一下:
-
初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。
-
因为t1线程频繁地从主存中读取run的值,jit即时编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run的访问以提高效率
-
1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量 的值,结果永远是旧值。
解决方法
volatile(表示易变关键字的意思),它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存 。
使用synchronized关键字也有相同的效果!在Java内存模型中,synchronized规定,线程在加锁时, 先清空工作内存→在主内存中拷贝最新变量的副本到工作内存 →执行完代码→将更改后的共享变量
-