在前面的文章中,我们深入了解了 Java 内存模型,知道了 Java 内存模型诞生的意义,以及其要解决的问题。最终我们知道:Java 内存模型就是定义了 8 个基本操作以及 8 个规则,只要遵守这些规则的并发操作,那么它们就是安全的。

即使强如树哥的人,看了这 16 条规则也很头疼。它们太过于繁琐了,非常不利于我们日常代码的编写。为了能帮助编程人员理解,于是就有了与其相等价的判断原则 —— 先行发生原则,它可以用于判断一个访问在并发环境下是否安全。

到这里,我们需要明白:happens-before 原则是对 Java 内存模型的简化,让我们更好地写出并发代码。就像 Java 语言等高级语言,之于汇编语言、机器语言等低级语言一样,可以让编程人员免受「皮肉之苦」。

什么是 happens-before?

happens-before 指的是 Java 内存模型中两项操作的顺序关系。例如说操作 A 先于操作 B,也就是说操作 A 发生在操作 B 之前,操作 A 产生的影响能够被操作 B 观察到。这里的「影响」包括:内存中共享变量的值、发送了消息、调用了方法等。

举个很简单的例子:下面代码里 i=1 在线程 A 中执行,而 j=i 在线程 B 中执行。因为 i=1 操作先于 j=i 执行,那么 i=1 操作的结果就应该能够被线程 B 观察到。

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

Java 内存模型下一共有 8 条 happens-before 规则,如果线程间的操作无法从如下几个规则推导出来,那么它们的操作就没有顺序性保障,虚拟机或者操作系统就能随意地进行重排序,从而可能会发生并发安全问题。

  • 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
  • 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。
  • volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。
  • 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
  • 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
  • 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
  • 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
  • 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

极简实践案例

Java 语言无须任何同步手段保障,就能成立的先行发生规则,就只有上面这些了。下面举个例子来说明如何用这些规则去判断操作是否具备顺序性,是否是线程安全的。

private int value = 0;

public void setValue(int value){
this.value = value;
} public int getValue(){
return value;
}

上面的代码是一组很普通的 getter/setter 方法。假设线程 A 和 B,线程 A 先(时间上的先后)调用了 setValue(1),之后线程 B 调用了同一个对象的 getValue(),那么线程 B 收到的返回值是什么?

我们依次分析一下先行发生原则中的各项规则:

  1. 首先,由于两个方法分别由线程A和线程B调用,不在一个线程中,所以程序次序规则在这里不适用。
  2. 接着,由于没有同步块,自然就不会发生lock和unlock操作,所以管程锁定规则不适用。
  3. 继续,由于value变量没有被volatile关键字修饰,所以volatile变量规则不适用。
  4. 继续,后面的线程启动、终止、中断规则和对象终结规则也和这里完全没有关系。
  5. 最后,因为没有一个适用的先行发生规则,所以最后一条传递性也无从谈起。

因此,即使我们知道线程 A 在操作时间上先于线程 B,但我们还是无法确定线程 B getValue() 方法的返回结果。换句话说,这里面的操作不是线程安全的。

那怎么修复这个问题呢?

我们至少有两种比较简单的方案可以选择:

  1. 第一种,要么把 getter/setter 方法都定义为 synchronized 方法,这样就可以套用管程锁定规则。
  2. 第二种,要么把 value 定义为 volatile 变量,由于 setter 方法对 value 的修改不依赖 value 的原值,满足 volatile 关键字使用场景,这样就可以套用 volatile 变量规则来实现先行发生关系。

通过上面这个案例,我们知道:一个操作时间上线发生,不代表这个操作会「先行发生」。 那如果一个操作「先行发生」,是否就能推导出这个操作必定是时间上先发生呢?其实并不能,因为有可能发生指令重排序。

// 如下操作在同一个线程中执行
int j = 1;
int j = 2;

上述代码在同一线程中执行,根据程序执行次序规则,int i = 1; 的操作先行发生于 int j = 2;,但 int j =2 的代码有可能被处理器先执行,因为它们不相互依赖,不影响先行发生原则的正确性。

上述这两个案例综合起来证明了一个结论:时间先后顺序与先行发生原则之间基本没有太大的关系,所以我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准。

总结

happens-before 原则一共有 8 条原则,它是对 Java 内存模型规则的简化,帮助编程人员提高编程效率。

时间先后顺序与先行发生原则之间基本没有太大的关系,我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准。

深入理解 happens-before 原则的更多相关文章

  1. 设计模式之里氏代换原则(LSP)

    里氏代换原则(Liskov Substitution Principle, LSP) 1 什么是里氏代换原则 里氏代换原则是由麻省理工学院(MIT)计算机科学实验室的Liskov女士,在1987年的O ...

  2. C#软件设计——小话设计模式原则之:单一职责原则SRP

    前言:上篇C#软件设计——小话设计模式原则之:依赖倒置原则DIP简单介绍了下依赖倒置的由来以及使用,中间插了两篇WebApi的文章,这篇还是回归正题,继续来写写设计模式另一个重要的原则:单一职责原则. ...

  3. 深入理解JavaScript中创建对象模式的演变(原型)

    深入理解JavaScript中创建对象模式的演变(原型) 创建对象的模式多种多样,但是各种模式又有怎样的利弊呢?有没有一种最为完美的模式呢?下面我将就以下几个方面来分析创建对象的几种模式: Objec ...

  4. Java程序员应当知道的10个面向对象设计原则

    面向对象设计原则是OOPS编程的核心, 但我见过的大多数Java程序员热心于像Singleton (单例) . Decorator(装饰器).Observer(观察者) 等设计模式,而没有把足够多的注 ...

  5. JAVA设计模式总结之六大设计原则

    从今年的七月份开始学习设计模式到9月底,设计模式全部学完了,在学习期间,总共过了两篇:第一篇看完设计模式后,感觉只是脑子里面有印象但无法言语.于是决定在看一篇,到9月份第二篇设计模式总于看完了,这一篇 ...

  6. java设计模式(2)---六大原则

    设计模式之六大原则 这篇博客非常有意义,希望自己能够理解的基础上,在实际开发中融入这些思想,运用里面的精髓. 先列出六大原则:单一职责原则.里氏替换原则.接口隔离原则.依赖倒置原则.迪米特原则.开闭原 ...

  7. Java设计模式——合成/聚合复用原则

    一.什么是合成/聚合复用原则? 合成/聚合复用原则是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分:新的对象通过向这些对象的委派达到复用已有功能的目的. 简述为:要尽量使用合成/聚合,尽 ...

  8. rsync --include-from --exclude-from的理解

    rsync --include-from --exclude-from的理解: 1.同时添加--include-from --exclude-from时.后者是对前者的结果进行排除 如:“--incl ...

  9. 设计模式原则(7)--Composition&AggregationPrinciple(CARP)--合成&聚合复用原则

    作者QQ:1095737364    QQ群:123300273     欢迎加入! 1.定义:  要尽量使用合成和聚合,尽量不要使用继承. 2.使用场景: 要正确的选择合成/复用和继承,必须透彻地理 ...

  10. 架构师修炼 III - 掌握设计原则

    关于软件的设计原则有很多,对于设计原则的掌握.理解.实践及升华是架构师的一项极为之必要的修炼. 记得在12年前第一次阅读<敏捷开发>时,五大基本设计原则就深深地植入到我的脑海中一直影响至今 ...

随机推荐

  1. fpm工具来制作rpm包软件

    第1章 rpm包的制作 1.1 fpm的概念介绍 FPM功能简单说就是将一种类型的包转换成另一种类型 1.1.1.支持的源类型 类型 说明 dir   将目录打包成所需要的类型,可以用于源码编译安装的 ...

  2. 手写一个bind

    1 Function.prototype.bind1 = function(){ 2 // 将类数组转化成数组 3 let arr = Array.prototype.slice.call(argum ...

  3. 我的树莓派 Raspberry Pi Zero 2W 折腾笔记,记录一些遇到的问题和解决办法

    0x00 前言说明 最近买了一块Raspberry Pi Zero 2W来玩,目的是想搭建一台远程运行的个人服务器,上面放个博客.点个灯啥的.于是就有了这篇文章. 树莓派官网地址:https://ww ...

  4. jmeter脚本编写

    jmeter脚本编写 一.http协议接口编写注意事项 1.请求体为json格式:一定要写请求头Content-Type:application/json 2.json格式文本 2.1 key-val ...

  5. mask-image实现聚光灯效果

    大家好,我是半夏,一个刚刚开始写文的沙雕程序员.如果喜欢我的文章,可以关注 点赞 加我微信:frontendpicker,一起学习交流前端,成为更优秀的工程师-关注公众号:搞前端的半夏,了解更多前端知 ...

  6. Linux-编译安装http-实验

    准备工作 1.关闭防火墙和SELinux 2.基础安装的系统,安装以下命令 yum install gcc make autoconf gcc-c++ glibc glibc-devel pcre p ...

  7. Kubernetes 从入门到进阶实战教程 (2021 最新万字干货版)

    作者:oonamao 毛江云,腾讯 CSIG 应用开发工程师原文:来源腾讯技术工程,https://tinyurl.com/ya3ennxf 写在前面 笔者今年 9 月从端侧开发转到后台开发,第一个系 ...

  8. 『现学现忘』Git基础 — 20、Git中忽略文件补充

    目录 1.忽略文件常遇到的问题 2.忽略文件配置优先级 3.忽略已跟踪文件的改动(本机使用) 4.autocrlf和safecrlf参数说明 (1)提出问题 (2)autocrlf说明 (3)safe ...

  9. Markdown基础语法(上)

    前言 按照官方文档,和根据自己所用和所理解所写 一.标题语法 一级标题最大,六级标题最小 # 一级标题 ## 二级标题 ### 三级标题 #### 四级标题 ##### 五级标题 ###### 六级标 ...

  10. docker 灵活的构建 php 环境

    地址: https://github.com/ydtg1993/server           使用docker搭建灵活的线上php环境 有时候你可能不太需要一些别人已经集成了的包或者镜像      ...