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

简介: 基于JSR -133内存模型提出了happens-before的概念,通过这个概念来阐述操作之间的内存可见性。要保证可见性,就是遵守 Happens-Before 规则,合理的使用java提供的工具。
我是石页兄,朋友不因远而疏,高山不隔友谊情;偶遇美羊羊,我们互相鼓励

欢迎关注微信公众号「架构染色」交流和学习

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

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

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

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

image.png

二、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 后面对这个变量的读操作。

image.png

volatile 变量规则(图片来自网络).png

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

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

    Thread B = new Thread(()->{
      // 此处对共享变量var修改
      var = 66;
    });
    // 例如此处对共享变量修改,
    // 则这个修改结果对线程B可见
    // 主线程启动子线程
    B.start();
    B.join()
    // 子线程所有对共享变量的修改
    // 在主线程调用B.join()之后皆可见
    // 此例中,var==66
  3. 线程中断规则(Thread Interruption Rule) 对线程 interrupt()方法的调用 Happens-Before 被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupt()方法检测到是否有中断发生。
  4. 对象终结规则(Finalizer Rule) 一个对象的初始化完成(构造函数执行结束)Happens-Before 它的 finalize()方法的开始。
  5. 传递性(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”。这意味着什么呢?

image.png

在 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。

五、最后说一句

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

欢迎点击链接扫马儿关注、交流。

参考并感谢

相关文章
|
2月前
|
存储 数据库 数据库管理
数据库事务安全性控制如何实现呢
【10月更文挑战第15天】数据库事务安全性控制如何实现呢
|
4月前
|
NoSQL Redis
Lettuce的特性和内部实现问题之在同步调用模式下,业务线程是如何拿到结果数据的
Lettuce的特性和内部实现问题之在同步调用模式下,业务线程是如何拿到结果数据的
|
5月前
|
Java
通用快照方案问题之调整Hystrix的信号量隔离模式的并发限制如何解决
通用快照方案问题之调整Hystrix的信号量隔离模式的并发限制如何解决
36 0
|
5月前
|
存储 设计模式 监控
Java面试题:如何在不牺牲性能的前提下,实现一个线程安全的单例模式?如何在生产者-消费者模式中平衡生产和消费的速度?Java内存模型规定了变量在内存中的存储和线程间的交互规则
Java面试题:如何在不牺牲性能的前提下,实现一个线程安全的单例模式?如何在生产者-消费者模式中平衡生产和消费的速度?Java内存模型规定了变量在内存中的存储和线程间的交互规则
53 0
|
Java
策略枚举:消除在项目里大批量使用if-else的优雅姿势
可以替换大量的if-else语句,且具备较好的可读性与扩展性,同时能显得轻量化,我比较推荐使用策略枚举来消除if-else。
137 0
|
SQL 安全 前端开发
案例07-在线人员列表逻辑混乱-ThreadLocal、继承、索引失效
案例07-在线人员列表逻辑混乱-ThreadLocal、继承、索引失效
|
存储 JSON 前端开发
Android数据库存储模块封装,让操作记录更好用可复用
Android数据库存储模块封装,让操作记录更好用可复用
|
缓存 Java 中间件
并发三大特性——可见性
并发三大特性——可见性
203 0
并发三大特性——可见性
|
SQL Oracle 关系型数据库
事务特性及隔离问题
事务特性及隔离问题
106 0
|
监控 安全 网络安全
可见性和分析在零信任架构中的作用
可见性和分析在零信任架构中的作用
183 0