1.Java内存模型
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、 CPU 指令优化等。
JMM体现在三方面
- 原子性-保证指令不会受到线程上下文切换的影响(Monitor 主要关注的是访问共享变量时,保证临界区代码的原子性)
- 可见性-保证指令不会受CPU缓存的影响
- 有序性-保证指令不会受CPU指令并行优化的影响
2.可见性
2.1循环无法退出
//测试实例,当修改flag值后,线程无法退出
public class TestLock {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (flag){
}
}, "t1");
System.out.println();
t1.start();
Thread.sleep(1000);
flag = false;
}
}
原因分析:初始状态, t 线程刚开始从主内存读取了 flag 的值到工作内存。因为 t 线程要频繁从主内存中读取 flag 的值,JIT 编译器会将 flag 的值缓存至自己工作内存中的高速缓存中, 减少对主存中 flag 的访问,提高效率。1 秒之后,main 线程修改了 flag 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值。
解决方法:volatile(易变关键字),它可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。
2.2 可见性与原子性的区别
可见性保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况。其只能保证看到最新值,不能解决指令交错。
注:
- synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低
- 死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到对 flag 变量的修改,原因是在println()方法中存在synchronized保护的同步代码块
3.有序性
3.1 指令重排现象
现象说明:JVM 会在不影响正确性的前提下,可以调整语句的执行顺序
static int i;
static int j;
// 在某个线程内执行如下赋值操作
//先执行 i 还是 先执行 j ,对最终的结果不会产生影响,故两者的顺序可能发生重排
i = ...;
j = ...;
解决方法:volatile 修饰的变量可以禁用指令重排
4.volatile 原理
volatile的底层实现原理是内存屏障
- 对volatile变量的写指令后会加入写屏障
- 对volatile变量的读指令前会加入读屏障
4.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;
}
}
4.2 有序性保障
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
注:volatile 不能解决指令交错
- 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
- 而有序性的保证也只是保证了本线程内相关代码不被重排序
5 happens-before规则
happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结。
- 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
static int x;
static Object m = new Object();
new Thread(()->{
synchronized(m) {
x = 10;
}
},"t1").start();
new Thread(()->{
synchronized(m) {
System.out.println(x);
}
},"t2").start();
- 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
volatile static int x;
new Thread(()->{
x = 10;
},"t1").start();
new Thread(()->{
System.out.println(x);
},"t2").start();
- 线程 start 前对变量的写,对该线程开始后对该变量的读可见
static int x;
x = 10;
new Thread(()->{
System.out.println(x);
},"t2").start();
- 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)
static int x;
Thread t1 = new Thread(()->{
x = 10;
},"t1");
t1.start();
t1.join();
System.out.println(x);
- 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过 t2.interrupted 或 t2.isInterrupted)
static int x;
public static void main(String[] args) {
Thread t2 = new Thread(()->{
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println(x);
break;
}
}
},"t2");
t2.start();
new Thread(()->{
sleep(1);
x = 10;
t2.interrupt();
},"t1").start();
while(!t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x);
}
- 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
- 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排
volatile static int x;
static int y;
new Thread(()->{
y = 10;
x = 20;
},"t1").start();
new Thread(()->{
// x=20 对 t2 可见, 同时 y=10 也对 t2 可见
System.out.println(x);
},"t2").start();