文章目录
上一章讲解的 Monitor 主要关注的是访问共享变量时,保证临界区代码的原子性。
这一章我们进一步深入学习共享变量在多线程间的【可见性】问题与多条指令执行时的【有序性】问题。
一、Java 内存模型
JMM 即 Java Memory Model,它从 Java 层面定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。JMM 体现在一下几个方面
- 原子性:保证指令不会受线程上下文切换的影响。
- 可见性:保证指令不回受 CPU 缓存的影响(JIT 堆热点代码的缓存优化)
- 有序性:保证指令不会受 CPU 指令并行优化的影响。
之前讲的 synchronized 底层 Monitor 主要关注的是访问共享变量时,保证临界区代码的原子性。
1.1 可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值。
1.1.1 举个例子:退不出的循环
import static java.lang.Thread.sleep;
public class Test1 {
private static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (run) {
}
});
t.start();
sleep(1000);
run = false;
}
}
为什么会发生这种情况,我们分析一下:
- 初始状态下,t 线程刚开始从内存读取了 run 的值到工作内存。
- 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率。
- 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取了这个变量的值,结果永远是旧值。
可见性问题,这里涉及到 Java 内存模型(JMM)
JMM 描述了 Java 程序中各种变量(线程共享变量)的访问规则,以及在 JVM 中将变量存储到内存和从内存中读取出变量这样的底层细节。
在内存模型中,所有的变量都存储在主内存中。每个线程都有自己独立的工作内存,里面保存着该线程使用到的变量的副本。
内存模型图:
JMM 线程操作内存的两条基本规定:
- 关于线程与主内存:线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写。
- 关于线程间工作内存:不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要经过主内存来完成。
共享变量可见性实现的原理:
线程 1 对共享变量的修改要想被线程 2 及时看到,必须要经过两个步骤:
- 把工作内存 1 中更新过的共享变量刷新到主内存中
- 把内存中最新的共享变量的值更新到工作内存 2 中。
1.1.2 解决方法
- 使用
volatile
(表示易变关键字的意思),它可以用来修饰成员变量
和静态成员变量
,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。 - 使用
synchronized
关键字也有相同的效果,在Java内存模型中,synchronized规定,线程在加锁时,需要先清空工作内存 -> 在主内存中拷贝最新变量的副本到工作内存 -> 执行完代码 -> 将更改后的共享变量的值刷新到主内存中 -> 释放互斥锁
1.1.3 可见性 vs 原子性
前面例子体现的实际就是可见性,它是指保证多个线程之间一个线程对volatile
变量的修改对另一个线程可见,而不能保证原子性。volatile用在一个写线程,多个读线程的情况,比较合适。
举个例子:两个线程一个i++,一个 i–,但是volatile只保证可见性,只能看到最新之,但不能解决指令交错问题(原子性)。
// 假设i的初始值为0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
synchronized
语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是它是属于重量级操作,性能相对较低。如果在前面实例的死循环中加入 System.out.println() 会发现即使不加volatile修饰符,线程t也能正确看到对run变量的修改了,想想是为什么?因为println方法里面有synchronized修饰。
1.1.4 模式之两阶段种植
我们之前的做法:当我们在执行线程一时,想要终止线程二,这就需要使用interrupt
方法来优雅的停止线程二。
使用volatile
关键字来实现两阶段终止模式。
public class Test1 {
public static void main(String[] args) throws InterruptedException {
// 下面是两个线程操作共享变量stop
Monitor monitor = new Monitor();
monitor.start();
Thread.sleep(3500);
monitor.stop();
}
}
class Monitor {
// private boolean stop = false; // 不会停止程序
private volatile boolean stop = false; // 会停止程序
/**
* 启动监控器线程