JMM使用happens-before(发生之前)的概念来阐述操作之间的内存可见性。让共享变量的写操作能够被其他线程的读操作可见,避免可见性问题,它是可见性与有序性的一套规则总结。
为什么会有这个规则呢
如果程序开发中,仅靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦。

程序的顺序性规则

按照顺序推演程序的执行结果,但是编译器未必一定会按照这个顺序编译,但是编译器保证结果一定==顺序推演的结果。

  • 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见

    1. // 共享变量
    2. static int x;
    3. public static void main(String[] args) {
    4. // t1线程对共享变量的赋值操作
    5. new Thread(() -> {
    6. // 共享变量的写操作
    7. x = 10;
    8. }, "t1").start();
    9. // t2线程对共享变量的读取操作
    10. new Thread(() -> {
    11. // t2线程在t1线程之后,t1线程执行结果能够被t2线程可见
    12. System.out.println(x);
    13. }, "t2").start();
    14. }
    1. 10

    监视器锁规则

    如果一个操作加锁在另一个操作发生之前解锁,那么第一个操作的执行结果将被第二个操作可见。

  • synchronized锁住了obj,需要两个线程执行完毕后才解锁,而在两个线程解锁 obj 之前,对变量的写操作都是会按照顺序执行,变量的写操作将是第一个操作。而t2线程要读取共享变量,但t2线程会一直是被t1线程阻塞住,直到t1线程写操作完成后,互斥锁释放,第二个操作就是对变量的读,对于接下来对 obj 加锁的其它线程对该变量的读可见

    1. public class HappensBeforeSequentialTest {
    2. // 共享变量
    3. static int x;
    4. static Object obj = new Object();
    5. public static void main(String[] args) {
    6. // t1线程对共享变量的赋值操作
    7. new Thread(()-> {
    8. // 保证原子性和可见性和有序性
    9. synchronized(obj) {
    10. // 共享变量的写操作
    11. x = 10;
    12. }
    13. },"t1").start();
    14. // t2线程对共享变量的读取操作
    15. new Thread(()-> {
    16. synchronized(obj) {
    17. // t2线程在t1线程之后,t1线程执行结果能够被t2线程可见
    18. System.out.println(x);
    19. }
    20. },"t2").start();
    21. }
    22. }
    1. 10

    volatile规则

  • 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见

volatile是把共享变量加到主内存中,所以能够被其他线程可见

  1. public class HappensBeforeVolatileTest {
  2. // 共享变量
  3. volatile static int x;
  4. public static void main(String[] args) {
  5. // t1线程对共享变量的赋值操作
  6. new Thread(() -> {
  7. // 共享变量的写操作
  8. x = 10;
  9. }, "t1").start();
  10. // t2线程对共享变量的读取操作
  11. new Thread(() -> {
  12. // t2线程在t1线程之后,t1线程执行结果能够被t2线程可见
  13. System.out.println(x);
  14. }, "t2").start();
  15. }
  16. }
  1. 10

线程启动规则

主线程mian启动线程t2,线程t2中可以看到主线程启动t2之前的操作。

  • 线程 start 前对变量的写,对该线程开始后对该变量的读可见

    1. // 共享变量
    2. static int x;
    3. public static void main(String[] args) {
    4. // 共享变量的写操作
    5. x = 10;
    6. // t2线程对共享变量的读取操作
    7. new Thread(() -> {
    8. // t2线程在t1线程之后,t1线程执行结果能够被t2线程可见
    9. System.out.println(x);
    10. }, "t2").start();
    11. }
    1. 10

    线程终结规则

    主线程main等待子线程t1完成,当子线程t1执行完毕后,主线程main可以看到线程B的所有操作。

  • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)

    1. // 共享变量
    2. static int x;
    3. public static void main(String[] args) throws InterruptedException {
    4. Thread t1 = new Thread(() -> {
    5. // 共享变量的写操作
    6. x = 10;
    7. }, "t1");
    8. t1.start();
    9. t1.join();
    10. System.out.println(x);
    11. }
    1. 10

    线程中断规则

  • 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过 t2.interrupted 或 t2.isInterrupted)

    1. // 共享变量
    2. static int x;
    3. public static void main(String[] args) throws InterruptedException {
    4. Thread t2 = new Thread(() -> {
    5. while (true) {
    6. if (Thread.currentThread().isInterrupted()) {
    7. System.out.println(x);
    8. break;
    9. }
    10. }
    11. }, "t2");
    12. t2.start();
    13. new Thread(() -> {
    14. try {
    15. Thread.sleep(1000);
    16. } catch (InterruptedException e) {
    17. e.printStackTrace();
    18. }
    19. x = 10;
    20. t2.interrupt();
    21. }, "t1").start();
    22. while (!t2.isInterrupted()) {
    23. Thread.yield();
    24. }
    25. System.out.println(x);
    26. }
    1. 10
    2. 10

    传递性规则

  • 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子

    1. // 共享变量
    2. volatile static int x;
    3. static int y;
    4. public static void main(String[] args) throws InterruptedException {
    5. new Thread(()->{
    6. y = 10;
    7. x = 20;
    8. },"t1").start();
    9. new Thread(()->{
    10. // x=20 对 t2 可见, 同时 y=10 也对 t2 可见
    11. System.out.println(x);
    12. },"t2").start();
    13. }
    1. 20