每一个线程有一个工作内存,和主存是独立的。 工作内存存放主存中变量的值则需要拷贝。线程独享的工作内存和主存的关系,如下图
happens-before 规则
happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见
线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
static int x;static Object m = new Object();public void test1() {new Thread(() -> {synchronized (m) {x = 10;}}, "t1").start();new Thread(() -> {synchronized (m) {System.out.println(x);}}, "t2").start();}
线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
volatile static int y;public void test2() {new Thread(() -> {y = 10;}, "t1").start();new Thread(() -> {System.out.println(y);}, "t2").start();}
线程 start 前对变量的写,对该线程开始后对该变量的读可见
static int x;public void test4() {x = 10;new Thread(() -> {System.out.println(x);}, "t2").start();}
线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待
它结束)static int x;public void test5() throws InterruptedException {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 test6() {Thread t2 = new Thread(() -> {while (true) {if (Thread.currentThread().isInterrupted()) {System.out.println(x);break;}}}, "t2");t2.start();new Thread(() -> {Sleeper.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;public void test7() {new Thread(() -> {y = 10;x = 20; //x加了写屏障}, "t1").start();new Thread(() -> {// x=20 对 t2 可见, 同时 y=10 也对 t2 可见System.out.println(x);}, "t2").start();}
原子性
保证指令不会受到线程上下文切换的影响
Java内存模型直接保证的原子性变量操作包括read,load,assign,use,store,write。大致可以认为基本数据类型的访问读写是具备原子性的,如果应用场景需要提供更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,虚拟机把这两种操作直接开放给用户使用,但是提供了更高层次的字节码指令:monitorenter和monitorexit来隐式的使用这两个操作,这两个字节码反映到Java代码中就是同步块——synchronized关键字,因此,在synchronized块之间的操作具备原子性可见性
保证指令不会受 cpu 缓存的影响static boolean run = true;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(run){// ....}});t.start();sleep(1);run = false; // 线程t不会如预想的停下来}
- 分析
- 初始状态,
t1线程刚开始从主内存读取了run的值到工作内存 - 因为
t1线程要频繁从主内存中读取run的值,JIT 编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run的访问,提高效率 - 1 秒之后,main 线程修改了
run的值,并同步至主存,而t1是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值。
- 解决方法
volatile它可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存synchronized实现可见性
JMM关于synchronized的两条规定:
- 线程解锁前,必须把共享变量的最新值刷新到主内存中
- 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值
(注意:加锁与解锁需要是同一把锁)
通过以上两点,可以看到synchronized能够实现可见性。同时,由于synchronized具有同步锁,所以它也具有原子性
volatile 原理
volatile的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
内存屏障类型 | 屏障类型 | 指令示例 | 说明 | | —- | —- | —- | | LoadLoad | Load1;LoadLoad;Load2 | 确保Load1的数据的装载先于Load2及所有后续装载指令的装载 | | StoreStore | Store1;StoreStore;Store2 | 确保Store1数据对其他处理器可见(刷新到内存)先于Store2及所有后续存储指令的存储 | | LoadStore | Load1;LoadStore;Store2 | 确保Load1的数据的装载先于Store2及所有后续存储指令的存储 | | StoreLoad | Store1;StoreLoad;Load2 | 确保Store1的数据对其他处理器可见(刷新到内存)先于Load2及所有后续的装载指令的装载 |
Store屏障
是x86的”sfence“指令,强制所有在store屏障指令之前的store指令,都在该store屏障指令执行之前被执行,并把store缓冲区的数据都刷到CPU缓存。简而言之,写屏障之前的写操作(写指令)会将cpu缓存区(L1+L2)中的数据刷到主存或者共享缓存中(L3缓存)
- Load屏障
是x86上的”ifence“指令,强制所有在load屏障指令之后的load指令,都在该load屏障指令执行之后被执行,并且一直等到load缓冲区被该CPU读完才能执行之后的load指令。简而言之,读屏障之前的读操作(读指令)会将cpu的无效指令队列中的指令清空,确保下一次读的数据是从L3缓存或者主存中读的
- 在每个
volatile写操作的前面插入一个StoreStore屏障,后面插入一个StoreLoad屏障,保证volatile写与之前的写操作指令不会重排序,写完数据之后立即执行flush处理器缓存操作将所有写操作刷到内存,对所有处理器可见。 - 在每个
volatilie读操作的前面插入一LoadLoad屏障,保证在该变量读操作时,如果其他处理修改过,必须从其他处理器高速缓存(或者主内存)加载到自己本地高速缓存,保证读取到的值是最新的。然后在该变量读操作后面插入一个LoadStore屏障,禁止volatile读操作与后面任意读写操作重排序。volatile实现的两条原则(保证可见性和禁止指令重排序)
- Lock前缀指令会引起处理器缓存回写到内存。lock前缀指令相当于一个内存屏障(也称内存栅栏),内存屏障主要提供3个功能:
- 确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 强制将对缓存的修改操作立即写入主存,利用缓存一致性机制,并且缓存一致性机制会阻止同时修改由两个以上CPU缓存的内存区域数据;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。
一个处理器的缓存回写到内存会导致其他处理器的缓存失效。处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。例如CPU A嗅探到CPU B打算写内存地址,且这个地址处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充。
volatile写和volatile读的内存语义
线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。
- 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
有序性
指令重排序
现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这 5 个阶段。在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行,指令重排的前提是,重排指令不能影响结果
// 可以重排的例子int a = 10; // 指令1int b = 20; // 指令2System.out.println( a + b );// 不能重排的例子int a = 10; // 指令1int b = a - 5; // 指令2
案例
会有一种情况线程2 执行flag=true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行x= 2 这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现(需借助java 并发压测工具 jcstress )
public class OrderRearrangement {boolean flag = false;int x = 0;//线程1public void actor1(Result result) {if (flag) {result.r = x + x;} else {result.r = 1;}System.out.println(result.r);}//线程2public void actor2() {x = 2;flag = true;}@Benchmarkpublic void test() {OrderRearrangement o = new OrderRearrangement();Result r = new Result();new Thread(() -> {o.actor1(r);}, "t1").start();new Thread(() -> {o.actor2();}, "t2").start();}}class Result {int r;public void setR(int r) {this.r = r;}public int getR() {return r;}}

