并发一直都是程序开发者绕不开的难题,在上一篇文章中我们知道了导致并发问题的源头是 : 多核 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 并发编程实战》

转载于:https://www.cnblogs.com/fanyi0922/p/11486580.html

happen before 原则相关推荐

  1. 深入研究ConcurrentHashMap 源码从7到8的变迁

    ConcurrentHashMap是线程安全且高效的HashMap 1 为什么要使用ConcurrentHashMap 线程不安全的HashMap HashMap是Java中最常用的一个Map类,性能 ...

  2. 面试再被问到 ConcurrentHashMap,把这篇文章甩给他!

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 来源:^_TONY_^ cnblogs.com/ITtangtan ...

  3. 转:【Java并发编程】之十六:深入Java内存模型——happen-before规则及其对DCL的分析(含代码)...

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/17348313 happen-before规则介绍 Java语言中有一个"先行发生 ...

  4. Java高并发编程(八):Java并发容器和框架

    1. ConcurrentHashMap 1.1 ConcurrentHashMap的优势 在并发编程中使用HashMap可能导致程序死循环.而使用线程安全的HashTable效率又非 常低下,基于以 ...

  5. ConcurrentHashMap之实现细节

    http://www.iteye.com/topic/344876 实现原理  锁分离 (Lock Stripping) ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁 ...

  6. Java并发容器(一) CocurrentHashMap的应用及实现

    转载自 http://blog.csdn.net/qq_24451605/article/details/51125946 CocurrentHashMap的优势 首先常用三种HashMap包括Has ...

  7. java中 d_Java 中的 syncronized 你真的用对了吗

    生活中随处可见并行的例子,并行 顾名思义就是一起进行的意思,同样的程序在某些时候也需要并行来提高效率,在上一篇文章中我们了解了 Java 语言对缓存导致的可见性问题.编译优化导致的顺序性问题的解决方法 ...

  8. 聊聊并发(四)深入分析ConcurrentHashMap

    转载自  聊聊并发(四)深入分析ConcurrentHashMap 术语定义 术语 英文 解释 哈希算法 hash algorithm 是一种将任意内容的输入转换成相同长度输出的加密方式,其输出被称为 ...

  9. 聊聊并发(四)——深入分析ConcurrentHashMap

    转载自:http://www.infoq.com/cn/articles/ConcurrentHashMap 术语定义 术语 英文 解释 哈希算法 hash algorithm 是一种将任意内容的输入 ...

  10. 并发容器与框架——并发容器(一)

    2019独角兽企业重金招聘Python工程师标准>>> Java中提供了非常多的并发容器和框架,在此仅对部分并发容器进行介绍. 1.ConcurrentHashMap的实现原理与使用 ...

最新文章

  1. u盘文件看得见却打不开_win7下u盘文件打不开怎么办 win7下u盘文件打不开解决方法...
  2. 《山谷物语》要怎么成为Supercell下一款年收入10亿美元的产品?
  3. python super()(转载)
  4. 高级Java必看的10本书
  5. java的常用引用类、数组、String类
  6. 现代软件工程 作业 原型设计
  7. js一些平时会用到的
  8. avue-crud 使用_创建和使用CRUD存储过程
  9. git21天打卡day4-查看仓库地址
  10. 软件开发工具--自考2019年4月
  11. excel服务器数据同步修改,勤哲Excel服务器同步解决海量数据快速上传问题
  12. Scala - Redis hgetAll 优化 by hscan
  13. 奇妙“水仙花数”的判断
  14. OFDM系统中的信道估计基础知识
  15. 南华大学计算机科学学院,万亚平
  16. 前端包管理器的领头大哥——npm
  17. 轻松省力清洁地面,分分钟搞定家务,云米智能洗地机Cyber Lite体验
  18. 【网络教程】Ubuntu20如何修改ip地址网关DNS
  19. 计算机应用基础搜题答案,2017计算机应用基础试题及答案
  20. 【Matlab】Matlab作图的一些小知识

热门文章

  1. 误入 GitHub 游戏区,结果意外地收获颇丰
  2. 英语语法总结_02 名词词组与代名词
  3. 签了工作之后才发现,自己太草率了  很长很真实 但会对你有所帮助的
  4. SEO新手入门必读,系统学习SEO知识理念
  5. CF1528B Kavi on Pairing Duty(dp)
  6. 正面管教读书笔记 10 你的性格对孩子性格的影响
  7. Windows server 2008 R2安装详细教程
  8. 数据中台在企业数字化转型中的践行(下篇)
  9. php jquery 时间轴,jquery时间轴
  10. python webdriver firefox 登录126邮箱,先添加联系人,然后进入首页发送邮件,带附件。...