前言

熟悉 Java 并发编程的都知道,JMM(Java 内存模型) 中的 happen-before(简称 hb)规则,该规则定义了 Java 多线程操作的有序性和可见性,防止了编译器重排序对程序结果的影响。

按照官方的说法:

当一个变量被多个线程读取并且至少被一个线程写入时,如果读操作和写操作没有 HB 关系,则会产生数据竞争问题。

要想保证操作 B 的线程看到操作 A 的结果(无论 AB 是否在一个线程),那么在 AB 之间必须满足 HB 原则,如果没有,将有可能导致重排序。

当缺少 HB 关系时,就可能出现重排序问题。

HB 有哪些规则?

这个大家都非常熟悉了应该,大部分书籍和文章都会介绍,这里稍微回顾一下:

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  2. 锁定规则:在监视器锁上的解锁操作必须在同一个监视器上的加锁操作之前执行。
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作;
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

其中,传递规则我加粗了,这个规则至关重要。如何熟练的使用传递规则是实现同步的关键

然后,再换个角度解释 HB:当一个操作 A HB 操作 B,那么,操作 A 对共享变量的操作结果对操作 B 都是可见的。

同时,如果 操作 B HB 操作 C,那么,操作 A 对共享变量的操作结果对操作 B 都是可见的。

而实现可见性的原理则是 cache protocol 和 memory barrier。通过缓存一致性协议和内存屏障实现可见性。

如何实现同步?

在 Doug Lea 著作 《Java Concurrency in Practice》中,有下面的描述:

书中提到:通过组合 hb 的一些规则,可以实现对某个未被锁保护变量的可见性。

但由于这个技术对语句的顺序很敏感,因此容易出错

楼主接下来,将演示如何通过 volatile 规则和程序次序规则实现对一个变量同步。

来一个熟悉的例子:

class ThreadPrintDemo {

  static int num = 0;
static volatile boolean flag = false; public static void main(String[] args) { Thread t1 = new Thread(() -> {
for (; 100 > num; ) {
if (!flag && (num == 0 || ++num % 2 == 0)) {
System.out.println(num);
flag = true;
}
}
}
); Thread t2 = new Thread(() -> {
for (; 100 > num; ) {
if (flag && (++num % 2 != 0)) {
System.out.println(num);
flag = false;
}
}
}
); t1.start();
t2.start();
}
}

这段代码的作用是两个线程间隔打印出 0 - 100 的数字。

熟悉并发编程的同学肯定要说了,这个 num 变量没有使用 volatile,会有可见性问题,即:t1 线程更新了 num,t2 线程无法感知。

哈哈,楼主刚开始也是这么认为的,但最近通过研究 HB 规则,我发现,去掉 num 的 volatile 修饰也是可以的。

我们分析一下,楼主画了一个图:

我们分析这个图:

  1. 首先,红色和黄色表示不同的线程操作。
  2. 红色线程对 num 变量做 ++,然后修改了 volatile 变量,这个是符合 程序次序规则的。也就是 1 HB 2.
  3. 红色线程对 volatile 的写 HB 黄色线程对 volatile 的读,也就是 2 HB 3.
  4. 黄色线程读取 volatile 变量,然后对 num 变量做 ++,符合 程序次序规则,也就是 3 HB 4.
  5. 根据传递性规则,1 肯定 HB 4. 所以,1 的修改对 4来说都是可见的。

注意:HB 规则保证上一个操作的结果对下一个操作都是可见的。

所以,上面的小程序中,线程 A 对 num 的修改,线程 B 是完全感知的 —— 即使 num 没有使用 volatile 修饰。

这样,我们就借助 HB 原则实现了对一个变量的同步操作,也就是在多线程环境中,保证了并发修改共享变量的安全性。并且没有对这个变量使用 Java 的原语:volatile 和 synchronized 和 CAS(假设算的话)。

这可能看起来不安全(实际上安全),也好像不太容易理解。因为这一切都是 HB 底层的 cache protocol 和 memory barrier 实现的。

其他规则实现同步

  1. 利用线程终结规则实现:
  static int a = 1;

  public static void main(String[] args) {
Thread tb = new Thread(() -> {
a = 2;
});
Thread ta = new Thread(() -> {
try {
tb.join();
} catch (InterruptedException e) {
//NO
}
System.out.println(a);
}); ta.start();
tb.start();
}
  1. 利用线程 start 规则实现:
  static int a = 1;

  public static void main(String[] args) {
Thread tb = new Thread(() -> {
System.out.println(a);
});
Thread ta = new Thread(() -> {
tb.start();
a = 2;
}); ta.start();
}

这两个操作,也可以保证变量 a 的可见性。

确实有点颠覆之前的观念。之前的观念中,如果一个变量没有被 volatile 修饰或 final 修饰,那么他在多线程下的读写肯定是不安全的 —— 因为会有缓存,导致读取到的不是最新的。

然而,通过借助 HB,我们可以实现。

总结

虽然本文标题是通过 happen-before 实现对共享变量的同步操作,但主要目的还是更深刻的理解 happen-before,理解他的 happen-before 概念其实就是保证多线程环境中,上一个操作对下一个操作的有序性和操作结果的可见性。

同时,通过灵活的使用传递性规则,再对规则进行组合,就可以将两个线程进行同步 —— 实现指定的共享变量不使用原语也可以保证可见性。虽然这好像不是很易读,但也是一种尝试。

关于如何组合使用规则实现同步,Doug Lea 在 JUC 中给出了实践。

例如老版本的 FutureTask 的内部类 Sync(已消失),通过 tryReleaseShared 方法修改 volatile 变量,tryAcquireShared 读取 volatile 变量,这是利用了 volatile 规则;

通过在 tryReleaseShared 之前设置非 volatile 的 result 变量,然后在 tryAcquireShared 之后读取 result 变量,这是利用了程序次序规则。

从而保证 result 变量的可见性。和我们的第一个例子类似:利用程序次序规则和 volatile 规则实现普通变量可见性。

而 Doug Lea 自己也说了,这个“借助”技术非常容易出错,要谨慎使用。但在某些情况下,这种“借助”是非常合理的。

实际上,BlockingQueue 也是“借助”了 happen-before 的规则。还记得 unlock 规则吗?当 unlock 发生后,内部元素一定是可见的。

而类库中还有其他的操作也“借助”了 happen-before 原则:并发容器,CountDownLatch,Semaphore,Future,Executor,CyclicBarrier,Exchanger 等。

总而言之,言而总之:

happen-before 原则是 JMM 的核心所在,只有满足了 hb 原则才能保证有序性和可见性,否则编译器将会对代码重排序。hb 甚至将 lock 和 volatile 也定义了规则。

通过适当的对 hb 规则的组合,可以实现对普通共享变量的正确使用。

Java 使用 happen-before 规则实现共享变量的同步操作的更多相关文章

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

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

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

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

  3. 【Java并发编程】:深入Java内存模型——happen-before规则及其对DCL的分析

    happen—before规则介绍 Java语言中有一个“先行发生”(happen—before)的规则,它是Java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,其意思就是说, ...

  4. Java标识符的命名规则

    Java语言中,为各种变量.方法和类等起的名字称为标识符Java标识符的命名规则:应以字母.下划线.美元符开头后跟字母.下划线.美元符或数字Java标识符大小写敏感,长度无限制 1.java中能用作标 ...

  5. Java包名命名规则

    1.  sun公司建议java包的命名规则为域名的倒写. 比如:sun公司www.sun.com 就用该是com.sun.www 2. indi : 个体项目,指个人发起,但非自己独自完成的项目,可公 ...

  6. 【Java】标识符 & 命名规则

    Java的标识符和命名规则 什么是标识符[Identifier]? 指用来标识某个实体的一个符号.在不同的应用环境下有不同的含义. 在编程语言中,标识符是开发者编程时使用的名字,对于变量.常量.函数. ...

  7. Java-如何不使用-volatile-和锁实现共享变量的同步操作

    from: http://thinkinjava.cn/2018/06/Java-%E5%A6%82%E4%BD%95%E4%B8%8D%E4%BD%BF%E7%94%A8-volatile-%E5% ...

  8. JNI/NDK开发指南(二)——JVM查找java native方法的规则

    通过第一篇文章,大家明白了调用native方法之前,首先要调用System.loadLibrary接口加载一个实现了native方法的动态库才能正常访问,否则就会抛出java.lang.Unsatis ...

  9. java中的一些规则(菜鸟的课堂笔记)

    ls 查看目录下文件 java规则 代码都定义在类中,用class定义 禁止一个源文件写两个类: 一个源文件中,只能有一个类文件是pubic: 一个源文件中如果有多个类,编译完之后会产生多个class ...

随机推荐

  1. Scrum冲刺阶段1

    各个成员在 Alpha 阶段认领的任务 人员 任务 何承华 美化设计 部分后端设计 陈宇 后端设计 丁培辉 美化设计 部分后端设计 温志铭 前端设计 杨宇潇 服务器搭建 张主强 前端设计 明日各个成员 ...

  2. 第34 memcached缓存

    1.缓存数据库 缓存:将数据存储在内存中,只有当磁盘胜任不了的时候,才会启用缓存. 缺点:断电数据丢失(双电),用缓存存储数据的目的只是为了应付大并发的业务,至于数据存储及可靠性不要找他了.   数据 ...

  3. 让IE8支持html5中的video标签

    这是一篇综合几个前辈的解决方案. 使用video的时候,要遇到的问题. ①不兼容ie9及其以下版本 在<head>里添加两行, 参考张鑫旭前辈的博客,但是在ie8中薄播放. <!-- ...

  4. ScriptOJ-unique#89

    一般做法 const unique = (arr) => { const result = arr.reduce((acc, iter) => { if(acc.indexOf(iter) ...

  5. Android Studio导入项目一直卡在Building gradle project info的解决方案

    出现了一个很神奇的现象,Android Studio导入其它项目均正常,但是导入某个项目(两天前还正常打开的项目)却一直卡在Building gradle project info 尝试了重启Andr ...

  6. windows 批处理语言学习

    程序员应该根植于心的一个理念是:重复的工作交给代码.windows上的批处理脚本就是这种理念的体现. 批处理bat能做的事很多,自动配置vs工程中的代码依赖环境,调用其它程序处理数据.自动编译代码等等 ...

  7. 《Nosql精粹》—— 读后总结

  8. mysql explain语法详解--优化你的查询

    原文地址:http://blog.csdn.net/zhuxineli/article/details/14455029 explain显示了mysql如何使用索引来处理select语句以及连接表.可 ...

  9. 第一篇:服务的注册与发现Eureka(Finchley版本)

    一.创建服务注册中心(Eureka) 1. 首先创建一个maven主工程 创建一个主Maven工程,在其pom文件引入依赖,spring Boot版本为2.0.3.RELEASE,Spring Clo ...

  10. SQL 的单引号转义字符

    SQL 的转义字符是:'(单引号) 例:select * from user where name = '''06' 其中红色的单引号即表示转义字符,上例中 name的实际条件值为 '06,而不是 ' ...