遵循Happens-Before规则来保证可见性|而非掌握所有底层

一、关注使用效果而非底层实现

前文《原来了解重排序是为了掌握可见性的保障》《运行期重排序:内存系统的重排序》《避免重排序之使用 Volatile 关键字》中梳理了一部分重排序对可见性的影响,这些内容只是保证可见性的一小部分内容,笔者本身还对寄存器缓存和指令并行重排等诸多细节有疑问。应该不少读者老师也能感受到梳理清楚所有的细节真的是相当有难度的。

作为上层高级语言程序员,的确很难掌握全部底层的情况。更希望结合 java 语法,以使用的视角通过一种更简单的方式来掌握如何保证可见性,而不是  如何实现可见性保证。 保证可见性的实现是 JMM 自己的事情,而不是程序员的。而 JMM 的 happens-before 的概念就是这个作用,规范程序员怎么使用以保障可见性。

从 JDK5 开始 java 使用新的 JSR -133 内存模型,并依据此内存模型提出了 happens-before 的概念,通过这个概念来阐述操作之间的内存可见性。程序员要保证可见性,就是遵守 Happens-Before 规则,合理的使用 java 提供的工具。

二、Happens-Before 概念

Java 内存模型中指定的 Happens-Before 规则,Happens-Before 规则最初是在一篇叫做 Time, Clocks, and the Ordering of Events in a Distributed System 的论文中提出来的,在这篇论文中,Happens-Before 的语义是一种因果关系。在现实世界里,如果 A 事件是导致 B 事件的起因,那么 A 事件一定是先于(Happens-Before)B 事件发生的,这个就是 Happens-Before 语义的现实理解。

在 Java 语言里面,Happens-Before 的语义本质上是一种可见性,A Happens-Before B 意味着 A 事件对 B 事件来说是可见的,无论 A 事件和 B 事件是否发生在同一个线程里。例如 A 事件发生在线程 1 上,B 事件发生在线程 2 上,Happens-Before 规则保证线程 2 上也能看到 A 事件的发生

三、Java 原生存在的 Happens-Before 规则

下边这 8 条规则是 Java 内存模型下存在的原生 Happens-Before 关系,无需借助任何同步器协助就已经存在,可以在编码中直接使用。

  1. 程序次序规则(Program Order Rule) 在一个线程内,按照程序代码顺序,书写在前面的操作 Happens-Before 书写在后面的操作

  2. 管程锁定规则(Monitor Lock Rule) An unlock on a monitor happens-before every subsequent lock on that monitor. 一个 unlock 操作 Happens-Before 后面对同一个锁的 lock 操作。思考,不是同一个锁就不保证了吗?是的

    synchronized (this) { //此处自动加锁
      // x是共享变量,初始值=10
      if (this.x < 12) {
        this.x = 12;
      }
    } //此处自动解锁
    
    //管程中锁的规则,可以这样理解:
    //假设x的初始值是10,线程A执行完代码块后x的值会变成12(执行完自动释放锁),
    //线程B进入代码块时,能够看到线程A对x的写操作,也就是线程B能够看到x==12
    复制代码
  3. volatile 变量规则(volatile Variable Rule) A write to a volatile field happens-before every subsequent read of that volatile. 对一个 volatile 变量的写入操作 Happens-Before 后面对这个变量的读操作。

  4. 线程启动规则(Thread Start Rule) Thread 对象的 start()方法 Happens-Before 此线程的每一个动作。

    Thread B = new Thread(()->{
      // 主线程调用B.start()之前
      // 所有对共享变量的修改,此处皆可见
      // 此例中,var==77
    });
    // 此处对共享变量var修改
    var = 77;
    // 主线程启动子线程
    B.start();
    复制代码
  5. 线程终止规则(Thread Termination Rule) 线程中的所有操作都 Happens-Before 对此线程的终止检测。

    Thread B = new Thread(()->{
      // 此处对共享变量var修改
      var = 66;
    });
    // 例如此处对共享变量修改,
    // 则这个修改结果对线程B可见
    // 主线程启动子线程
    B.start();
    B.join()
    // 子线程所有对共享变量的修改
    // 在主线程调用B.join()之后皆可见
    // 此例中,var==66
    复制代码
  6. 线程中断规则(Thread Interruption Rule) 对线程 interrupt()方法的调用 Happens-Before 被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupt()方法检测到是否有中断发生。

  7. 对象终结规则(Finalizer Rule) 一个对象的初始化完成(构造函数执行结束)Happens-Before 它的 finalize()方法的开始。

  8. 传递性(Transitivity) 偏序关系的传递性:如果已知 hb(a,b)和 hb(b,c),那么我们可以推导出 hb(a,c),即操作 a Happens-Before 操作 c。 class VolatileExample { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true) { // 这里x会是多少呢? } } } 根据程序次序规则 + volatile 变量规则+传递性,我们得到结果:“x=42” Happens-Before 读变量“v=true”。这意味着什么呢?

在 Java 语言中无需任何同步手段保障就能成立的先行发生规则就只有上面这些了。

四、推导更多的 Happens-Before

Java 中原生满足 Happens-Before 关系的规则就只有上述 8 条,但是还可以通过它们推导出其它的满足 Happens-Before 的操作,如:

  1. 将一个元素放入一个线程安全的队列的操作 Happens-Before 从队列中取出这个元素的操作
  2. 将一个元素放入一个线程安全容器的操作 Happens-Before 从容器中取出这个元素的操作
  3. 在 CountDownLatch 上的倒数操作 Happens-Before CountDownLatch#await()操作
  4. 释放 Semaphore 许可的操作 Happens-Before 获得许可操作
  5. Future 表示的任务的所有操作 Happens-Before Future#get()操作
  6. 向 Executor 提交一个 Runnable 或 Callable 的操作 Happens-Before 任务开始执行操作
  7. 如果两个操作之间不存在上述的 Happens-Before 规则中的任意一条,并且也不能通过已有的 Happens-Before 关系推到出来,那么这两个操作之间就没有顺序性的保障,虚拟机可以对这两个操作进行重排序!

如果存在 hb(a,b),那么操作 a 在内存上面所做的操作(如赋值操作等)都对操作 b 可见,即操作 a 影响了操作 b。

五、最后说一句

我是石页兄,如果这篇文章对您有帮助,或者有所启发的话,欢迎关注笔者的微信公众号【 架构染色 】进行交流和学习。您的支持是我坚持写作最大的动力。

原文作者:Applehope
链接:https://juejin.cn/post/7183292181531066425

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值