happen before 原则
并发一直都是程序开发者绕不开的难题,在上一篇文章中我们知道了导致并发问题的源头是 : 多核 CPU 缓存导致程序的可见性问题、多线程间切换带来的原子性问题以及编译优化带来的顺序性问题。
原子性问题我们暂且不谈,Java 中有足够健壮的锁机制来保证程序的原子性,后面学习的重点也是在这方面。今天我们就先来看看 Java 是怎么解决可见性与顺序性问题的。
合理的建议是 按需禁用缓存与编译优化,但是怎么才算是按需禁用呢?这就要具体问题具体分析了。Java 内存模型也规定了 JVM 如何按需提供禁用缓存与编译优化的方法,这些方法就是 volatile、syncronized、final 三个关键字与 happen-before 原则。
volatile
volatile 其实是一种稍弱的同步机制,在并发场景下它并不能保证线程安全。
加锁机制既可以保证可见性又可以保证原子性,而 volatile 变量则只能保证可见性。
在 jdk1.5 之前 volatile 给我们最深刻的印象就是 禁用缓存,即每次读写都是直接操作内存(不是 CPU 缓存),从内存中取值。Java 从 jdk1.5 开始对 volatile 进行了优化, 我们先来看下面的例子,再讨论优化了什么和怎么优化的。
假设有线程 A 和线程 B,A 线程调用 write(),将 flag 赋值为 true,B 线程调用 read() ,根据 volatile 定义变量的可见性,B 线程中 flag 为 true 所以会进入if 判断,那么此时的输出 index 的值是多少呢?
class VolatileTest{
int index = 0;
volatile boolean flag = false;
public void write(){
index = 10;
flag = true;
}
public void read(){
if(flag){
System.out.println(index);
}
}
}
这个问题在 jdk1.5 之前,显示的结果可能为 0,也可能为 10,原因在于 CPU 缓存导致的可见性问题,flag 是可以保证可见性,但是 index 却无法保证,当 A 线程执行完写进 CPU 缓存还没有更新的内存时,此时 B 线程读出的 index 值就是 0。
为了解决上述问题,从 jdk1.5 开始对 volatile 修饰的变量进行了优化, 在 jdk1.5 以后 此时 index 输出结果就是 10 。
到底是怎么优化的呢? 答案是 happen-before 原则中的传递性规则。
happen before 原则
happen before 并不是字面的英文的意思,想要理解它必须知道 happen before 的含义,它真正的意思是前面的操作对后续的操作都是可见的,比如 A happen before B 的意思并不是说 A 操作发生在 B 操作之前,而是说 A 操作对于 B 操作一定是可见的。
知道了它的意思,我们再来看一下 happen before 原则中与开发相关的几项规则:
程序的顺序性规则
这条规则很简单,也符合我们常规的思维,就是说在一个线程中程序的执行顺序,前面的操作对后面的操作是可见的,同样是上面的代码,在执行 write() 方法时,index = 10 对 flag = true 是可见的,如下 :
class VolatileTest{
int index = 0;
volatile boolean flag = false;
public void write(){
index = 10; // index 赋值 对 flag 是可见的
flag = true;
}
public void read(){
if(flag){
System.out.println(index);
}
}
}
volatile 变量规则
volatile 修饰的变量,对该变量的写操作一定可见于对该变量的读操作。
同样是上面的代码,A 线程执行 write() 为 flag 变量赋值为 true,B 线程执行 read() 方法,那么此时的 flag 一定为true,与 A B 线程执行顺序无关。
传递性
传递性就是 jdk1.5 之后对 volatile 语义的增强。啥叫传递性呢 ?
举个简单的数学比大小的例子,你就明白了。
有 a、b、c 三个数,如果 a > b, b > c, 那么由传递性可知 a > c。
这里的传递性的意思与例子类似, A 操作 happen before 于 B 操作,B 操作 happen before 于 C 操作,那么 A 操作 happen before 于 C 操作。同样是上面的代码,我们来解释下为什么 在 jdk1.5 之后 B 线程执行 read() 方法打印的结果一定为 10 呢?如图:

根据第一条程序的顺序性规则可知,A线程中 index = 10 对 flag = true 可见;
根据第二条 volatile 规则可知,flag 使用 volatile 修饰的变量, A 线程 flag 写 对 B 线程 flag 的读可见;
综合以上两条规则:
index = 10 happen before 于 flag 的写 ,
A 线程 flag 的写 happen before 于 B 线程 flag 的读,
根据传递性 , A 线程 对 index 的写 happen before 于 B 线程 对 flag 的读。所以 A 线程对 index 变量的写操作对 B 线程 flag 变量的读操作是可见的,即 B 线程读取 index 的打印结果为 10 。
管程中锁的规则
管程是一种通用的同步原语,管程在 java 中指的就是 syncronized。这条规则说的是 线程的解锁 happen before 于对这个线程的加锁。
也就是说线程的解锁对线程的加锁是可见的,那么在一个线程中操作的变量,在这个线程解锁后,根据传递性规则,当另一个线程加锁的时候,就会读到上一个线程对这个变量的操作后的新值。
线程的 start 规则
A 线程中调用 B 线程的 start(), 则 start() 操作 happen before 于 B 线程所有操作,也就是说 A 线程调用 start() 操作前对共享变量的赋值,对 B 线程可见,即在 B 线程中能获取 A 对到共享变量操作后的新值。
线程的 join 规则
A线程中调用B线程的 join() 操作,如果成功返回 则 B的所有操作对 join() 结果的返回一定是可见的,即在 A 线程能获取到 B 对共享变量操作后的新值。
除了以上列举几条规则,happen before 还有些其他的规则,在开发中不常用到,这里我们就不一一列举了。其实 Java 内存模型大体可以分为两部分,一部分是编写并发程序的开发人员,另一部分是 面向JVM 的实现人员 。当然我们更关注前者,而前者的核心就是 happen before 原则,常用的就是上面我们列举的原则,了解这些也就可以了。
在编译器优化方面使用的最多的就是 final 了, final 修饰变量就是常量,因此编译器可以使劲的优化,在 jdk1.5 以后 Java 内存模型对 final 类型变量的重排进行了约束。现在只要我们提供正确构造函数没有“逸出”,就不会出问题了。
总结:
happen before 原则核心就是可见性,并不是说 一个操作发生在另一个操作前面,它真正要表达的是:前面一个操作的结果对后续操作是可见的。

参考资料 : 《JAVA 并发编程实战》
happen before 原则的更多相关文章
- jvm(三)指令重排 & 内存屏障 & 可见性 & volatile & happen before
参考文档: https://tech.meituan.com/java-memory-reordering.html http://0xffffff.org/2017/02/21/40-atomic- ...
- Java集合---ConcurrentHashMap原理分析
集合是编程中最常用的数据结构.而谈到并发,几乎总是离不开集合这类高级数据结构的支持.比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存作为外部文件的副本(HashMap).这篇文章主 ...
- Java并发集合的实现原理
本文简要介绍Java并发编程方面常用的类和集合,并介绍下其实现原理. AtomicInteger 可以用原子方式更新int值.类 AtomicBoolean.AtomicInteger.AtomicL ...
- java并发编程(十六)happen-before规则
转载请注明出处:http://blog.csdn.net/ns_code/article/details/17348313 happen-before规则介绍 Java语言中有一个"先行发生 ...
- 深入分析ConcurrentHashMap
术语定义 哈希算法 hash algorithm 是一种将任意内容的输入转换成相同长度输出的加密方式,其输出被称为哈希值. 哈希表 hash table 根据设定的哈希函数H(key)和处理冲突方 ...
- 并发编程 02—— ConcurrentHashMap
Java并发编程实践 目录 并发编程 01—— ThreadLocal 并发编程 02—— ConcurrentHashMap 并发编程 03—— 阻塞队列和生产者-消费者模式 并发编程 04—— 闭 ...
- 深入分析ConcurrentHashMap(转)
线程不安全的HashMap 因为多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap,如以下代码 final HashMap ...
- ConcurrentHashMap总结
线程不安全的HashMap 因为多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap,如以下代码 final HashM ...
- 转:【Java并发编程】之十六:深入Java内存模型——happen-before规则及其对DCL的分析(含代码)
转载请注明出处:http://blog.csdn.net/ns_code/article/details/17348313 happen-before规则介绍 Java语言中有一个"先行发生 ...
随机推荐
- idea 2018.3.4安装破解
电脑环境:win10 64位 1.idea官网下载: 链接:https://www.jetbrains.com/idea/,如下图: 2.JDK官网下载: 链接:https://www.oracle. ...
- cdh5-MariaDB 配置(暂未排版)
在多数分布MariaDB的设施默认设置使用保守的缓冲区的大小和内存使用. 使用保守的缓冲区大小和内存使用率 Cloudera的数据库管理服务器,监控活动,报告管理,Cloudera 导航,Hive 的 ...
- 缓存的有效期和淘汰策略【Redis和其他缓存】【刘新宇】
缓存有效期与淘汰策略 有效期 TTL (Time to live) 设置有效期的作用: 节省空间 做到数据弱一致性,有效期失效后,可以保证数据的一致性 Redis的过期策略 过期策略通常有以下三种: ...
- JavaScript数据结构——树的实现
在计算机科学中,树是一种十分重要的数据结构.树被描述为一种分层数据抽象模型,常用来描述数据间的层级关系和组织结构.树也是一种非顺序的数据结构.下图展示了树的定义: 在介绍如何用JavaScript实现 ...
- Redhat Linux 系统上安装JDK 1.7
作者:潇湘隐者 出处:http://www.cnblogs.com/kerrycode/ 步骤1:下载JDK 1.7 安装包 JDK 1.7 下载地址:http://www.oracle.com/t ...
- 洛谷 P2572 [SCOI2010]序列操作
题意简述 维护一个序列,支持如下操作 把[a, b]区间内的所有数全变成0 把[a, b]区间内的所有数全变成1 把[a,b]区间内所有的0变成1,所有的1变成0 询问[a, b]区间内总共有多少个1 ...
- 重学计算机组成原理(七)- 程序无法同时在Linux和Windows下运行?
既然程序最终都被变成了一条条机器码去执行,那为什么同一个程序,在同一台计算机上,在Linux下可以运行,而在Windows下却不行呢? 反过来,Windows上的程序在Linux上也是一样不能执行的 ...
- 3.php基础(控制语句,函数,数组遍历)
if条件判断语句 结构一:只判断true,不管false 结构二:既判断true,也判断false(二选一) 结构三:多条件判断 switch多分支结构 Switch语法结构说明: l Switch的 ...
- python方法的使用
1.函数定义 def 函数名(形参): 函数体 return 返回值 2.函数执行 ...
- 监听input实时输入
// 用Jquery实现: $('#input-element').on('input',()=>{ console.log("你按了一下111");}) // 用Jquer ...