每一个线程有一个工作内存,和主存是独立的。 工作内存存放主存中变量的值则需要拷贝。线程独享的工作内存和主存的关系,如下图

image.pngimage.png

happens-before 规则

happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见

  1. 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见

    1. static int x;
    2. static Object m = new Object();
    3. public void test1() {
    4. new Thread(() -> {
    5. synchronized (m) {
    6. x = 10;
    7. }
    8. }, "t1").start();
    9. new Thread(() -> {
    10. synchronized (m) {
    11. System.out.println(x);
    12. }
    13. }, "t2").start();
    14. }
  2. 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见

    1. volatile static int y;
    2. public void test2() {
    3. new Thread(() -> {
    4. y = 10;
    5. }, "t1").start();
    6. new Thread(() -> {
    7. System.out.println(y);
    8. }, "t2").start();
    9. }
  3. 线程 start 前对变量的写,对该线程开始后对该变量的读可见

    1. static int x;
    2. public void test4() {
    3. x = 10;
    4. new Thread(() -> {
    5. System.out.println(x);
    6. }, "t2").start();
    7. }
  4. 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待
    它结束)

    1. static int x;
    2. public void test5() throws InterruptedException {
    3. Thread t1 = new Thread(() -> {
    4. x = 10;
    5. }, "t1");
    6. t1.start();
    7. t1.join();
    8. System.out.println(x);
    9. }
  5. 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过
    t2.interrupted 或 t2.isInterrupted)

    1. static int x;
    2. public static void test6() {
    3. Thread t2 = new Thread(() -> {
    4. while (true) {
    5. if (Thread.currentThread().isInterrupted()) {
    6. System.out.println(x);
    7. break;
    8. }
    9. }
    10. }, "t2");
    11. t2.start();
    12. new Thread(() -> {
    13. Sleeper.sleep(1);
    14. x = 10;
    15. t2.interrupt();
    16. }, "t1").start();
    17. while (!t2.isInterrupted()) {
    18. Thread.yield();
    19. }
    20. System.out.println(x);
    21. }
  6. 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见

  7. 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子
    1. volatile static int x;
    2. static int y;
    3. public void test7() {
    4. new Thread(() -> {
    5. y = 10;
    6. x = 20; //x加了写屏障
    7. }, "t1").start();
    8. new Thread(() -> {
    9. // x=20 对 t2 可见, 同时 y=10 也对 t2 可见
    10. System.out.println(x);
    11. }, "t2").start();
    12. }

    原子性

    保证指令不会受到线程上下文切换的影响
    Java内存模型直接保证的原子性变量操作包括read,load,assign,use,store,write。大致可以认为基本数据类型的访问读写是具备原子性的,如果应用场景需要提供更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,虚拟机把这两种操作直接开放给用户使用,但是提供了更高层次的字节码指令:monitorenter和monitorexit来隐式的使用这两个操作,这两个字节码反映到Java代码中就是同步块——synchronized关键字,因此,在synchronized块之间的操作具备原子性

    可见性

    保证指令不会受 cpu 缓存的影响
    1. static boolean run = true;
    2. public static void main(String[] args) throws InterruptedException {
    3. Thread t = new Thread(()->{
    4. while(run){
    5. // ....
    6. }
    7. });
    8. t.start();
    9. sleep(1);
    10. run = false; // 线程t不会如预想的停下来
    11. }
  • 分析
  1. 初始状态,t1线程刚开始从主内存读取了 run的值到工作内存
  2. 因为t1线程要频繁从主内存中读取 run的值,JIT 编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run的访问,提高效率
  3. 1 秒之后,main 线程修改了run的值,并同步至主存,而 t1是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值。
  • 解决方法
    volatile它可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile变量都是直接操作主存

    synchronized实现可见性

    JMM关于synchronized的两条规定:
  1. 线程解锁前,必须把共享变量的最新值刷新到主内存中
  2. 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值

(注意:加锁与解锁需要是同一把锁)
通过以上两点,可以看到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缓存或者主存中读的

  1. 在每个volatile写操作的前面插入一个StoreStore屏障,后面插入一个StoreLoad屏障,保证volatile写与之前的写操作指令不会重排序,写完数据之后立即执行flush处理器缓存操作将所有写操作刷到内存,对所有处理器可见。
  2. 在每个volatilie读操作的前面插入一LoadLoad屏障,保证在该变量读操作时,如果其他处理修改过,必须从其他处理器高速缓存(或者主内存)加载到自己本地高速缓存,保证读取到的值是最新的。然后在该变量读操作后面插入一个LoadStore屏障,禁止volatile读操作与后面任意读写操作重排序。

    volatile实现的两条原则(保证可见性和禁止指令重排序)

  • Lock前缀指令会引起处理器缓存回写到内存。lock前缀指令相当于一个内存屏障(也称内存栅栏),内存屏障主要提供3个功能:
    1. 确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
    2. 强制将对缓存的修改操作立即写入主存,利用缓存一致性机制,并且缓存一致性机制会阻止同时修改由两个以上CPU缓存的内存区域数据;
    3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。
  • 一个处理器的缓存回写到内存会导致其他处理器的缓存失效。处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。例如CPU A嗅探到CPU B打算写内存地址,且这个地址处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充。

    volatile写和volatile读的内存语义

  • 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。

  • 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
  • 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

    有序性

    保证指令不会受 cpu 指令并行优化的影响

    指令重排序

    现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这 5 个阶段。在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序组合来实现指令级并行,指令重排的前提是,重排指令不能影响结果

    1. // 可以重排的例子
    2. int a = 10; // 指令1
    3. int b = 20; // 指令2
    4. System.out.println( a + b );
    5. // 不能重排的例子
    6. int a = 10; // 指令1
    7. int b = a - 5; // 指令2
  • 案例

会有一种情况线程2 执行flag=true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行x= 2 这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现(需借助java 并发压测工具 jcstress )

  1. public class OrderRearrangement {
  2. boolean flag = false;
  3. int x = 0;
  4. //线程1
  5. public void actor1(Result result) {
  6. if (flag) {
  7. result.r = x + x;
  8. } else {
  9. result.r = 1;
  10. }
  11. System.out.println(result.r);
  12. }
  13. //线程2
  14. public void actor2() {
  15. x = 2;
  16. flag = true;
  17. }
  18. @Benchmark
  19. public void test() {
  20. OrderRearrangement o = new OrderRearrangement();
  21. Result r = new Result();
  22. new Thread(() -> {
  23. o.actor1(r);
  24. }, "t1").start();
  25. new Thread(() -> {
  26. o.actor2();
  27. }, "t2").start();
  28. }
  29. }
  30. class Result {
  31. int r;
  32. public void setR(int r) {
  33. this.r = r;
  34. }
  35. public int getR() {
  36. return r;
  37. }
  38. }