一、让人又爱又恨的指令重排

了解过Java并发编程知识的童鞋都知道,Java内存模型是围绕着并发过程中如何处理原子性、可见性和有序性3个特征来建立的,其中有序性最为复杂。
我们习惯性的认为代码总是从先到后、依次执行的,这在单线程的时候确实是没错的(至少程序是正确的运行的)。但在并发时,有时候给人感觉写在后面的代码,比写在前面的代码先执行,如同出现了幻觉。这就是鼎鼎大名的指令重排,指令重排是很有必要的,因为大大提高了cpu处理性能。
然而,指令重排,在提高了性能的同时,也会发生一些意想不到的灾难,举个栗子:

class UnsafeOrderExample {int x = 0;boolean v = false;public void writer() {x = 42;v = true;}public void reader() {if (v == true) {// 这里 x 会是多少呢?System.out.print(x);}}
}

对于上面的代码,如果只有一个线程,先执行writer方法,然后再执行reader方法,会得到一个跟预期一致的结果是42。
但假设有两个线程A、B,两者同时分别执行writer和reader方法,最终reader会输出什么呢?很显然,答案是不固定的,有可能输出42,也有可能输出0;这是因为writer方法中的代码有可能发生指令重排,导致v=true有可能会发生在x=42之前。这个类是线程不安全类。
指令重排是必要的,但同时它又带来了一些麻烦,这可怎么办?别急,Java对此指定了Happens-Before规则,既然不能禁止指令重排,那就用规则对指令重排作约束,正所谓“爱,就是克制”嘛。

二、Happens-Before规则

正如前面所说,虽然jvm和执行系统会对指令进行一定的重排,但也是建立在一些原则上的,并非所有指令都可以随便改变执行位置。这些原则就是Happens-Before原则。Happens-Before可以直译为“先行发生”,但其想表达的更深层的意思是“前面操作的结果对后续操作是可见的”。所以比较正式的说法是:Happens-Before约束了编译器的优化行为,虽然允许编译器优化,但是编译器优化后一定要遵循Happens-Before原则。

1、程序顺序规则

程序顺序原则,指的是在一个线程内,按照程序代码的顺序,前面的代码运行的结果能被后面的代码可见。(准确的说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。)
举个简单栗子:

int a,b
a=1
b=a+1  //如果指令重排不遵循程序顺序原则,则b有可能等于1

如果指令重排不遵循程序顺序原则,以上的代码的b最终有可能等于1,而不是我们期望的2。这个原则就保证了程序语义的正确性,重排指令不允许改掉原来的代码语义。

2、传递性

传递性,指的是如果A Happens-Before于B,B Happens-Before于C,则A Happens-Before于C。这个是很好理解的。用白话说就是,如果A的操作结果对B可见,B操作结果对C可见,则A的操作结果对C也是可见的。

3、volatile变量规则

指对一个volatile变量的写操作,Happens-Before于后续对这个volatile变量的读操作。如果单单是理解这句话的意思,就是我们熟悉的禁用cpu缓存的意思,使得volatile修饰的变量读到永远是最新的值。
如果这个规则跟第二个规则“传递性”结合来看,会有什么效果呢?我们可以通过改一下上面的例程来看看:

class UnsafeExample {int x = 0;volatile boolean v = false;//v用volatile修饰public void writer() {x = 42;v = true;}public void reader() {if (v == true) {// 这里 x 会是多少呢?System.out.print(x);}}
}

对比这段代码跟第一个例子中的代码,变化的只是成员变量v用了volatile修饰,如果仅仅是用“volatile变量规则”来看,如果同样是线程A、B同时分别调用writer和reader,得到的不也是有42或者0两个结果么?
别慌,如果我们再结合“传递性”规则来看:

  • x=42 Happens-Before 于写变量v=true
  • 写变量v=true Happens-Before 于读变量 v=true

根据“传递性”,可以得出x=42 Happens-Before 于读变量v=true,是不是恍然大悟了呢?由此可以得出最终B线程执行的reader方法输出的x=42而不是0。而这个结果,是靠“volatile变量规则”+“传递性”推导出来的,凭直觉是比较难看出来的。经过这样一番修改后,这个类就变成了线程安全了。

4、锁规则

锁规则,指的是一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
举个栗子:

synchronized (this) { // 此处自动加锁// x 是共享变量, 初始值 =10if (this.x < 12) {this.x = 12; }
} // 此处自动解锁

假设线程A执行完synchronized代码块后,x的值变成了12,线程B进入代码块时,可以看到线程A对x的修改,也就是能读到x==12。这个比较容易理解。

5、线程start()规则

指的是主线程A启动子线程B后,子线程B能看到主线程在启动线程B前的操作。
举个栗子:

Thread B = new Thread(()->{// 主线程调用 B.start() 之前// 所有对共享变量的修改,此处皆可见// 此例中,var==77
});
// 此处对共享变量 var 修改
var = 77;
// 主线程启动子线程
B.start();

此处,线程B能读到var==77。

6、线程join()规则

这个规则跟上一条规则有点类似,只不过这个规则是跟线程等待相关的。指的是主线程A等待子线程B完成(对B线程join()调用),当子线程B操作完成后,主线程A能看到B线程的操作。
举个栗子:

Thread B = new Thread(()->{// 此处对共享变量 var 修改var = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程 B 可见
// 主线程启动子线程
B.start();
B.join()
// 子线程所有对共享变量的修改
// 在主线程调用 B.join() 之后皆可见
// 此例中,var==66

此处,主线程A能看到线程B对共享变量var的操作,也就是可以看到var==66。

7、线程的interrupt()规则

指的是线程A调用线程B的interrupt()方法,Happens-Before 于线程B检测中断事件(也就是Thread.interrupted()方法)。这个也很容易理解。

8、finalize()规则

指的是对象的构造函数执行、结束 Happens-Before 于finalize()方法的开始。

三、总结

Happens-Before原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要一句,依靠这个原则,我们可以解决并发环境下两个操作之间是否存在冲突的所有问题。

Happens-Before原则相关推荐

  1. 关于新技术的引入原则 ——从零开始学架构

    不以解决实际问题引入的技术都耍流氓. 新技术的引入不是为了证明自己,而是为了解决实际项目中遇到的问题.希望诸位能够控制住自己的心魔. 新技术的引入要求应该是解决的问题大于带来的问题. 再引入新技术,请 ...

  2. JVM内存调优原则及几种JVM内存调优方法

    JVM内存调优原则及几种JVM内存调优方法 1.堆大小设置. 2.回收器选择. 1.在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因 ...

  3. 写好 Python 代码的几条原则

    程序设计的好与坏,早在我们青葱岁月时就接触过了,只是那是并不知道这竟如此重要.能够立即改善程序设计.写出"好"代码的知识有以下几点: •面向对象五个基本原则: •常见的三种架构: ...

  4. Python七大原则,24种设计模式

    七大设计原则: 1.单一职责原则[SINGLE RESPONSIBILITY PRINCIPLE]:一个类负责一项职责.  2.里氏替换原则[LISKOV SUBSTITUTION PRINCIPLE ...

  5. 7.12 其他面向对象设计原则3: 依赖倒置原则DIP

    其他面向对象设计原则3: 依赖倒置原则DIP  The Dependency Inversion Principle 7.1 依赖倒置原则DIP The Dependency Inversion P ...

  6. 设计模式---(设计原则)面向对象设计原则

    1 开闭原则 开闭原则:一个软件实体应当对扩展开放,对修改关闭. 在设计一个模块的时候,应当是这个模块可以再不被修改的前提下被扩展,换句话说就是,应当可以再不必修改源代码的情况下改变这个模块的行为. ...

  7. 敏捷宣言遵循的十二条原则

    敏捷宣言遵循的十二条原则Twelve Principles behind the Agile Manifesto 我们遵循以下原则: We follow these principles: 我们最重要 ...

  8. 【推荐】极限编程的十二大原则——小版本

    小版本:用最少的代码工作量带来最大的业务价值. 这个原则是意思是为了高度迭代,与客户展现开发的进展,小版本发布是一个可交流的好办法,客户可以针对性提出反馈.但小版本把模块缩得很小,会影响软件的整体思路 ...

  9. 大型企业门户网站设计开发一般性原则和建议

    [适用范围] 本文所述的原则.建议适用于大型企业信息门户网站的设计和开发,注意不是小型企业网站.一般企业电子商务网站.企业级Web应用系统. [一般性原则] 一.网站设计原则 第一原则:内容丰富.明确 ...

  10. 计算机设计原则,CISSP备考系列之计算机设计原则[10-39]

    [CISSP是小众,与MCSE,CCNA一类的不同,资料很少.本人在准备CISSP考试.总结一些考点,供大家参考(内容主要是<CISSP认证考试权威指南(第4版)>的读书笔记,感谢作者和译 ...

最新文章

  1. jupyter notebook多维数组运算_Python创建二维数组的正确姿势
  2. python写管理系统-基于Python实现用户管理系统
  3. 【五】MongoDB管理之生产环境说明
  4. PMCAFF产品众测 | 对话随手攒CEO聊聊这款产品的设计、推广和改进(活动已结束)
  5. Pytorch:GAN生成对抗网络实现二次元人脸的生成
  6. span的取值与赋值(原生js与jquery) - 对比篇
  7. Spring MVC 学习笔记2 - 利用Spring Tool Suite创建一个web 项目
  8. 浅拷贝直接赋值_Python里面如何拷贝一个对象?(赋值,浅拷贝,深拷贝的区别)?...
  9. 哲理故事300篇 中
  10. 滤镜之瓷砖TileReflect
  11. 酒店客房管理系统JAVA-SSM-MYSQL
  12. MySQL安装步骤(ZIP版)
  13. R语言isprime函数素数(prime number)判断实战
  14. 6.824:FaRM笔记
  15. sprintf函数详解
  16. 如何防止form表单重复提交
  17. 注入winlogon
  18. Windows 注册表操作 reg 命令详解
  19. android getruntime.exec 权限
  20. Python中仅跳出本次遍历或循环继续进入下一次遍历或循环continue语句

热门文章

  1. 为什么推广效果无法提升?
  2. AJAX异步判断注册用户名是否重复
  3. 第三章数程序设计初步--分支结构项目3利息计算器
  4. proxy代理报错: Uncaught (in promise) Error: Request failed with status code 500
  5. html5游戏一键端,【稀有游戏】伊卡洛斯ol手工端+VM一键端+伊卡洛斯客户端
  6. 一行代码制作自己的QQ/微信二维码,还可以制作动态的哟~(小白再也不用去求别人了)
  7. 全球及中国电动车头盔行业销售前景态势及投资盈利分析报告2021-2027年
  8. 基于java爱宠医院管理系统计算机毕业设计源码+系统+lw文档+mysql数据库+调试部署
  9. 我的第一次直播,聊聊「职业规划」和「个人成长」
  10. ps cs6导出html,Photoshop CS6新功能:软件设置和预置迁移