遵循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。

五、最后说一句

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

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

参考并感谢

相关文章
|
存储 Linux 调度
确保并发执行的安全性:探索多线程和锁机制以构建可靠的程序
在当今计算机系统中,多线程编程已成为常见的需求,然而,同时也带来了并发执行的挑战。为了避免数据竞争和其他并发问题,正确使用适当的锁机制是至关重要的。通过阅读本文,读者将了解到多线程和锁机制在并发编程中的重要性,以及如何避免常见的并发问题,确保程序的安全性和可靠性。通过实际案例和代码示例来说明如何正确地使用多线程和锁机制来构建可靠的程序。
52 1
|
2月前
|
安全 算法 Java
多线程写入同一个文件时,如何保证写入正常
【9月更文挑战第3天】多线程写入同一个文件时,如何保证写入正常
378 8
|
4月前
|
安全 Java
Volatile不保证原子性及解决方案
**原子性在并发编程中确保操作不可中断,保持数据一致性。volatile保证可见性但不保证原子性,如`count++`在多线程环境下仍可能导致数据不一致。解决方案包括使用`synchronized`、`AtomicInteger`或`ReentrantLock`来确保复合操作的原子性和线程安全。例子展示了volatile在并发自增中的局限性,实际值通常小于预期,强调了正确选择同步机制的重要性。**
|
4月前
|
存储 设计模式 监控
Java面试题:如何在不牺牲性能的前提下,实现一个线程安全的单例模式?如何在生产者-消费者模式中平衡生产和消费的速度?Java内存模型规定了变量在内存中的存储和线程间的交互规则
Java面试题:如何在不牺牲性能的前提下,实现一个线程安全的单例模式?如何在生产者-消费者模式中平衡生产和消费的速度?Java内存模型规定了变量在内存中的存储和线程间的交互规则
47 0
|
6月前
|
缓存 NoSQL Java
【亮剑】分布式锁是保证多服务实例同步的关键机制,常用于互斥访问共享资源、控制访问顺序和系统保护,如何使用注解来实现 Redis 分布式锁的功能?
【4月更文挑战第30天】分布式锁是保证多服务实例同步的关键机制,常用于互斥访问共享资源、控制访问顺序和系统保护。基于 Redis 的分布式锁利用 SETNX 或 SET 命令实现,并考虑自动过期、可重入及原子性以确保可靠性。在 Java Spring Boot 中,可通过 `@EnableCaching`、`@Cacheable` 和 `@CacheEvict` 注解轻松实现 Redis 分布式锁功能。
115 0
|
安全 Java
【并发技术09】原子性操作类的使用
【并发技术09】原子性操作类的使用
volatile 的作用是什么?能保证原子性吗?能保证有序性吗?
volatile 的作用是什么?能保证原子性吗?能保证有序性吗?
113 0
|
缓存 Java 中间件
并发三大特性——可见性
并发三大特性——可见性
181 0
并发三大特性——可见性
|
监控 安全 网络安全
可见性和分析在零信任架构中的作用
可见性和分析在零信任架构中的作用
175 0
|
安全 Java 数据安全/隐私保护
这8种保证线程安全的技术你都知道吗?(下)
这8种保证线程安全的技术你都知道吗?(下)