java 内存模型
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念。
JMM 体现在以下几个方面
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响
可见性
main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:
private static boolean run = true;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (run) {
// 死循环 一直占用 CPU
// 如果去掉 下面注释,将无法看到 效果 (猜测可能是 下面的方法无法一直占用CPU)
// System.out.println("循环");
}
});
t.start();
Sleep.sleep(1);
// 循环不会停下
run = false;
}
- 初始状态,t 线程刚开始从主内存读取了 run 的值到工作内存。
- 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中, 减少对主存中 run 的访问,提供频率。
- 1秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
线程的工作内存
线程的工作内存(working memory) 只是对 cpu 的寄存器和高速缓存的抽象描述。它并不是线程的栈。
内存一致性解决方法
volatile
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
有序性
JVM 会在不影响正确性的前提下,可以调整语句的执行顺序
static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = 5;
j = 3;
可以看到,至于是先执行 i 还是先执行 j,对最终的结果不会产生影响。所以执行顺序可能是
i = 5;
j = 3;
也可以是
j = 3;
i = 5;
这种特性称之为 【指令重排】,多线程下【指令重排】可能会影响正确性。为什么要有重排,这是依据计算机底层的执行指令的原理。
int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(IResult r) {
if(ready) {
r.r1 = num + num;
}else {
r.r1 = 1;
}
}
// 线程2 执行此方法
public void actor2(IResult r) {
num = 2;
ready = true;
}
class IResult {
public int r1;
}
可能的结果有【这里基本可以不用考虑可见性问题】:
情况1:线程1 先执行 结果为 1
情况2:线程2 先执行 num = 2,但没来得及执行 ready = true ,线程1 执行,还是进入 else 分支,结果为 1
情况3:线程2 执行到 ready = true,线程 1 执行,这回进入 if 分支,结果为 4
情况 为 4
线程2 执行 ready = true ,切换到线程1,进入 if 分支,相加为0,再切回线程2 执行 num = 2
这种现象叫指令重排,是 JIT 编译器在运行是的一些优化。
解决方法
volatile 修饰的变量,可以禁用指令重排