⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://cmsblogs.com/?p=2122 「小明哥」欢迎转载,保留摘要,谢谢!

作为「小明哥」的忠实读者,「老艿艿」略作修改,记录在理解过程中,参考的资料。


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

在上篇博客(《【死磕 Java 并发】—– 深入分析 volatile 的实现原理》)中,LZ 提到过由于存在线程本地内存和主内存的原因,再加上重排序,会导致多线程环境下存在可见性的问题。那么我们正确使用同步、锁的情况下,线程 A 修改了变量 a ,何时对线程 B 可见?

我们无法就所有场景来规定,某个线程修改的变量何时对其他线程可见,但是我们可以指定某些规则,这规则就是 happens-before 。从 JDK 5 开始,JMM 就使用 happens-before 的概念,来阐述多线程之间的内存可见性。

在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系。

happens-before 原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们解决在并发环境下两操作之间是否可能存在冲突的所有问题。下面我们就一个简单的例子稍微了解下happens-before ;

i = 1; // 线程 A 执行
j = i; //线程 B 执行

j 是否等于 1 呢?假定线程 A 的操作(i = 1)happens-before 线程 B 的操作(j = i),那么可以确定,线程 B 执行后 j = 1 一定成立。如果他们不存在 happens-before 原则,那么 j = 1 不一定成立。这就是happens-before原则的威力。

1. 定义

happens-before 原则【定义】如下:

    1. 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果,将对第二个操作可见,而且第一个操作的执行顺序,排在第二个操作之前。
    1. 两个操作之间存在 happens-before 关系,并不意味着一定要按照 happens-before 原则制定的顺序来执行。如果重排序之后的执行结果与按照 happens-before 关系来执行的结果一致,那么这种重排序并不非法。

2. 规则

happens-before 原则【规则】如下:

FROM 《深入理解 Java 虚拟机》

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作,happens-before 于书写在后面的操作。
  • 锁定规则:一个 unLock 操作,happens-before 于后面对同一个锁的 lock 操作。
  • volatile 变量规则:对一个变量的写操作,happens-before 于后面对这个变量的读操作。
  • 传递规则:如果操作 A happens-before 操作 B,而操作 B happens-before 操作C,则可以得出,操作 A happens-before 操作C
  • 线程启动规则:Thread 对象的 start 方法,happens-before 此线程的每个一个动作。
  • 线程中断规则:对线程 interrupt 方法的调用,happens-before 被中断线程的代码检测到中断事件的发生。
  • 线程终结规则:线程中所有的操作,都 happens-before 线程的终止检测,我们可以通过Thread.join() 方法结束、Thread.isAlive() 的返回值手段,检测到线程已经终止执行。
  • 对象终结规则:一个对象的初始化完成,happens-before 它的 finalize() 方法的开始

上面八条是原生 Java 满足 happens-before 关系的规则,但是我们可以对他们进行推导出其他满足 happens-before 的规则:

  1. 将一个元素放入一个线程安全的队列的操作,happens-before 从队列中取出这个元素的操作。
  2. 将一个元素放入一个线程安全容器的操作,happens-before 从容器中取出这个元素的操作。
  3. 在 CountDownLatch 上的 countDown 操作,happens-before CountDownLatch 上的 await 操作。
  4. 释放 Semaphore 上的 release 的操作,happens-before 上的 acquire 操作。
  5. Future 表示的任务的所有操作,happens-before Future 上的 get 操作。
  6. 向 Executor 提交一个 Runnable 或 Callable 的操作,happens-before 任务开始执行操作。

这里再说一遍 happens-before 的概念:如果两个操作不存在上述(前面8条 + 后面6条)任一一个 happens-before 规则,那么这两个操作就没有顺序的保障,JVM 可以对这两个操作进行重排序。如果操作 A happens-before 操作 B,那么操作A在内存上所做的操作对操作B都是可见的。

下面就用一个简单的例子,来描述下 happens-before 的原则:

private int i = 0;

public void write(int j ) {
i = j;
}

public int read() {
return i;
}

我们约定线程 A 执行 #write(int j),线程 B 执行 #read(),且线程 A 优先于线程 B 执行,那么线程 B 获得结果是什么?

就这段简单的代码,我们来基于 happens-before 的规则做一次分析:

  1. 由于两个方法是由不同的线程调用,所以肯定不满足程序次序规则。
  2. 两个方法都没有使用锁,所以不满足锁定规则。
  3. 变量 i 不是用volatile修饰的,所以 volatile 变量规则不满足。
  4. 传递规则肯定不满足。
  5. 规则 5、6、7、8 + 推导的 6 条可以忽略,因为他们和这段代码毫无关系。

所以,我们无法通过 happens-before 原则,推导出线程 A happens-before 线程 B 。虽然,可以确认在时间上,线程 A 优先于线程 B 执行,但是就是无法确认线程B获得的结果是什么,所以这段代码不是线程安全的

那么怎么修复这段代码呢?满足规则 2、3 任一即可。


happen-before原则是JMM中非常重要的原则,它是判断数据是否存在竞争、线程是否安全的主要依据,保证了多线程环境下的可见性。

下图是 happens-before 与 JMM 的关系图:

FROM 《Java并发编程的艺术》

happens-before

参考资料

  1. 周志明:《深入理解Java虚拟机》
  2. 方腾飞:《Java并发编程的艺术》

666. 彩蛋

整理本小节,简单脑图如下:脑图

如果你对 Java 并发感兴趣,欢迎加入我的知识星球一起交流。

知识星球

文章目录
  1. 1. 1. 定义
  2. 2. 2. 规则
  3. 3. 参考资料
  4. 4. 666. 彩蛋