本章内容

上一章讲解的 Monitor 主要关注的是访问共享变量时,保证临界区代码的原子性
这一章我们进一步深入学习共享变量在多线程间的【可见性】问题与多条指令执行时的【有序性】问题

Java 内存模型

JMM 即 Java Memory Model,它定义了主存工作内存抽象概念,底层对应着 CPU 寄存器缓存硬件内存CPU 指令优化等。
JMM 体现在以下几个方面

  • 原子性 - 保证指令不会受到线程上下文切换的影响
  • 可见性 - 保证指令不会受 cpu 缓存的影响
  • 有序性 - 保证指令不会受 cpu 指令并行优化的影响

    可见性

    image.png
    image.png
    image.png

    解决方法

    volatile(易变关键字)
    它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。

    可见性 vs 原子性

    image.png

    有序性

    JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码
    ```java static int i; static int j; // 在某个线程内执行如下赋值操作 i = …; j = …;
  1. 可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是
  2. ```java
  3. i = ...;
  4. j = ...;

也可以是

  1. j = ...;
  2. i = ...;

这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。为什么要有重排指令这项优化呢?从 CPU 执行指令的原理来理解一下吧
image.png
image.png
image.png

原理之指令级并行

诡异的结果

  1. int num = 0;
  2. boolean ready = false;
  3. // 线程1 执行此方法
  4. public void actor1(I_Result r) {
  5. if(ready) {
  6. r.r1 = num + num;
  7. } else {
  8. r.r1 = 1;
  9. }
  10. }
  11. // 线程2 执行此方法
  12. public void actor2(I_Result r) {
  13. num = 2;
  14. ready = true;
  15. }

I_Result 是一个对象,有一个属性 r1 用来保存结果,问,可能的结果有几种?
有同学这么分析

情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1
情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1
情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)

但我告诉你,结果还有可能是 0 😁😁😁,信不信吧!
这种情况下是:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2
相信很多人已经晕了 😵😵😵

这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现:
借助 java 并发压测工具 jcstress https://wiki.openjdk.java.net/display/CodeTools/jcstress

  1. mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jcstress -
  2. DarchetypeArtifactId=jcstress-java-test-archetype -DarchetypeVersion=0.5 -DgroupId=cn.itcast -
  3. DartifactId=ordering -Dversion=1.0

解决方法

volatile 修饰的变量,可以禁用指令重排

原理之 volatile

volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

  • 对 volatile 变量的写指令后会加入写屏障
  • 对 volatile 变量的读指令前会加入读屏障

image.png
image.png
image.png
image.png
image.png

double-checked locking

image.png
以上的实现特点是:

  • 懒惰实例化
  • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
  • 有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外

double-checked locking 解决

image.png

happens-before

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

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

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

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

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

    1. static int x;
    2. Thread t1 = new Thread(()->{
    3. x = 10;
    4. },"t1");
    5. t1.start();
    6. t1.join();
    7. System.out.println(x);
  • 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过 t2.interrupted 或 t2.isInterrupted)
    ```java static int x; public static void main(String[] args) { Thread t2 = new Thread(()->{ while(true) {

    1. if(Thread.currentThread().isInterrupted()) {
    2. System.out.println(x);
    3. break;
    4. }

    } },”t2”); t2.start(); new Thread(()->{ sleep(1); x = 10; t2.interrupt(); },”t1”).start(); while(!t2.isInterrupted()) { Thread.yield(); } System.out.println(x); }

  1. - 对变量默认值(0falsenull)的写,对其它线程对该变量的读可见
  2. - 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子
  3. ```java
  4. volatile static int x;
  5. static int y;
  6. new Thread(()->{
  7. y = 10;
  8. x = 20;
  9. },"t1").start();
  10. new Thread(()->{
  11. // x=20 对 t2 可见, 同时 y=10 也对 t2 可见
  12. System.out.println(x);
  13. },"t2").start();

单例习题

image.png
image.png
image.png
image.png