Happens-Before规则详解

简介: Happens-Before规则详解

本文为《Java高并发》第四篇文章,首发于个人网站


在《Java并发编程Bug的源头》一节中提到编译优化会带来有序性问题,具体来说就是 JIT 编译器会进行指令重排序(Instruction Reorder)优化。优化措施引发的有序性问题,Java 语言肯定会注意到,所以就引入了 Happens-Before(先行发生) 原则,它是  JMM 最核心的概念,在 JMM 章节中提到了如何保证可见性和有序性,都和该原则有关联。


对应 Java 程序员来说,理解 Happens-before 是理解 JMM 的关键。这个原则非常重要,它是判断数据是否存在竞争,线程是否安全的非常有用的手段。依赖这个原则,我们可以通过几条简单规则一并解决并发环境下两个操作之间是否可能存在冲突的所有问题,而不需要陷入 Java 内存模型苦涩难懂的定义之中。


JMM的设计


现在就来看看“先行发生”原则指的是什么。先行发生是 Java 内存模型中定义的两项操作之间的偏序关系,比如说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。这句话不难理解,但它意味着什么呢?我们通过一个简单的案例来进行演示。


double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r; // C
复制代码


上述代码用来计算圆的面积,存在3个 happens-before 关系,如下。

  • A happens-before B
  • B happens-before C
  • A happens-before C

在3个 happens-before 关系中,2和3是必需的,但1是不必要的。因此,JMM 把 happens-before 要求禁止的重排序分为了下面两类。

  • 会改变程序执行结果的重排序。
  • 不会改变程序执行结果的重排序。

JMM 对这两种不同性质的重排序,采取了不同的策略,如下。

  • 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
  • 对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(JMM允许这种重排序)。


综合来看,JMM 其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。例如,如果编译器经过细致的分析后,认定一个锁只会被单个线程访问,那么这个锁可以被消除,我们之前学习 JIT 编译器逃逸分析时有提到。


Happens-Before 规则


下面是 Java 内存模型下一些“天然的”先行发生关系, 这些先行发生关系无须任何同步器协助就已经存在, 可以在编码中直接使用。 如果两个操作之间的关系不在此列, 并且无法从下列规则推导出来, 则它们就没有顺序性保障, 虚拟机可以对它们随意地进行重排序。


1、程序次序规则:在一个线程中,前面的操作 Happens-Before 于后续的任意操作。


2、volatile变量规则:对一个 volatile 变量的写操作 Happens-Before 于对这个 volatile 变量的读操作。


3、传递性规则:A Happens-Before B,B Happens-Before C,那么 A Happens-Before C。


4、管程锁定规则:synchronized 是 Java 对管程的实现,隐式加锁、释放锁,对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。


关于管程的介绍:在操作系统中,管程的定义如下: 管程是由一组数据以及定义在这组数据之上的对该组数据操作的操作组成的软件模块,称之为管程。 基本特性: 1. 局部于管程的数据只能被局部于管程内的过程所访问。 2. 一个进程只有通过调用管程内的过程才能进入管程访问共享数据 3. 每次仅允许一个进程在管程中执行某个内部过程。 注意:由于管程是一个语言的成分,所以管程的互斥访问完全由编译程序在编译时自动添加,无需程序员关注。


而在 Java 中,管程指的就是 synchronized,synchronized 是 Java 里对管程的实现。

管程中的锁在 Java 里是隐式实现的,例如下面的代码,在进入同步块之前,会自动加锁,而在代码块执行完会自动释放锁,加锁以及释放锁都是编译器帮我们实现的。


synchronized (this) { //此处自动加锁
  // x是共享变量,初始值=10
  if (this.x < 12) {
    this.x = 12; 
  }  
} //此处自动解锁
复制代码


5、线程启动规则:Thread 对象的 start()方法先行发生于此线程的每一个动作。


主线程A启动子线程B后,子线程的 start()操作 Happens-Before于子操作中的任意操作,即子线程 B 能够看到主线程在启动子线程 B 前的操作。


Thread B = new Thread(()->{
  // 主线程调用B.start()之前
  // 所有对共享变量的修改,此处皆可见
  // 此例中,var==77
});
// 此处对共享变量var修改
var = 77;
// 主线程启动子线程
B.start();
复制代码


在上述代码中,main 线程启动子线程B后,B线程的 start()操作 Happens-Before于B线程操作中的任意操作,即线程 B 能够看到主线程在启动线程 B 前的操作。


6、线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过 Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。


Thread B = new Thread(()->{
  // 此处对共享变量var修改
  var = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程B可见
// 主线程启动子线程
B.start();
B.join()
// 子线程所有对共享变量的修改
// 在主线程调用B.join()之后皆可见
// 此例中,var==66
复制代码


7、线程中断规则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted()方法检测到是否有中断发生。


public class InterruptedSleepingTest {
  public static void main(String[] args) throws InterruptedException {
    InterruptedSleepingThread thread = new InterruptedSleepingThread();
    thread.start();
    // 10s后执行中断操作
    Thread.sleep(10000);
    thread.interrupt();
  }
}
class InterruptedSleepingThread extends Thread {
  @Override
  public void run() {
    doAPseudoHeavyWeightJob();
  }
  private void doAPseudoHeavyWeightJob() {
    for (int i = 0; i < Integer.MAX_VALUE; i++) {
      // You are kidding me
      System.out.println(i + " " + i * 2);
      // Let me sleep <evil grin>
      if (Thread.currentThread().isInterrupted()) {
        System.out.println("Thread interrupted\n Exiting...");
        break;
      } else {
        sleepBabySleep();
      }
    }
  }
  protected void sleepBabySleep() {
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      //当主线程中的interrupt方法执行之后,才会抛出
      Thread.currentThread().interrupt();
    }
  }
}
复制代码


执行结果为:


0 0
1 2
2 4
3 6
4 8
5 10
6 12
7 14
8 16
9 18
10 20
Thread interrupted
 Exiting...
复制代码


关于线程 interrupt 方法的详细讲解,可以参考这篇文章


8、对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize()方法的开始。


public class ObjectHappensTest {
  public int num;
  public String name;
  public ObjectHappensTest(int num, String name) {
    System.out.println("可以多次执行构造方法");
    this.num = num;
    this.name = name;
  }
  @Override
  protected void finalize() throws Throwable {
    System.out.println("进入finalize方法,只会执行一次");
    super.finalize();
  }
  public static void main(String[] args) throws InterruptedException {
    ObjectHappensTest obj;
//    obj = new ObjectHappensTest(30, "constructor");
    obj = null;
    System.gc();
    Thread.sleep(2000);
  }
}
复制代码


执行上述代码,什么也没有输出;如果取消注释,则会打印如下结果:


可以多次执行构造方法
进入finalize方法,只会执行一次
复制代码


证实了对象终结规则:一个对象在被垃圾回收之前必须已经进过初始化,垃圾回收不可能也不能去回收一个根本不存在的对象


扩展


如何保证一个共享变量的可见性?

1、保证共享变量的可见性,使用volatile关键字修饰即可,不管是针对该共享变量加 volatile,还是通过传递性来保证可见性,都算是 volatile 的功效。

2、保证共享变量是private,访问变量使用set/get方法,使用synchronized对两个方法加锁,此种方法不仅保证了可见性,也保证了线程安全

3、如果变量类型为 int,使用原子变量,例如:AtomicInteger等

4、利用线程的 join()方法或 start()方法



目录
相关文章
|
2月前
|
人工智能 数据中心 芯片
Gartner®报告:2025年阿里云公有云服务市场份额亚太第一
近日,Gartner 发布《2025年全球 IaaS 公有云服务市场份额》报告。报告显示,2025 年中国 IaaS 市场规模按营收达到 469.5 亿美元,阿里云以 32.8% 的市场份额位居第一,较 2024 年的 30.1% 提升 2.7 个百分点。在亚太市场,阿里云市场份额由 20.8% 提升至 22.5%。
|
Python 机器学习/深度学习
Cross Entropy Loss 交叉熵损失函数公式推导
表达式 输出标签表示为{0,1}时,损失函数表达式为: $L = -[y log \hat{y} + (1-y)log(1- \hat{y})]$ 二分类 二分类问题,假设 y∈{0,1} 正例:$P(y = 1| x) = \hat{y}$ 反例:$P(y=0|x) = 1-\hat{y}$ 取似然函数 似然函数就是所有样本在参数θ下发生概率最大的那种情况,由于样本独立同分布,因此概率最大的情况就是每个样本发生概率的连乘。
16943 0
|
网络协议 网络安全 PHP
使用天猫精灵实现计算机WOL网络唤醒
解决笔记本连显示器不想掀盖子开机和远程办公时给公司电脑开机不方便的痛点。
16196 8
使用天猫精灵实现计算机WOL网络唤醒
|
10月前
|
Ubuntu 物联网 Linux
从零安装一个Linux操作系统几种方法,以Ubuntu18.04为例
一切就绪后,我们就可以安装操作系统了。当系统通过优盘引导起来之后,我们就可以看到跟虚拟机中一样的安装向导了。之后,大家按照虚拟机中的顺序安装即可。 好了,今天主要介绍了Ubuntu Server版操作系统的安装过程,关于如何使用该操作系统,及操作系统更深层的原理,还请关注本号及相关圈子。
|
8月前
|
机器学习/深度学习 自然语言处理 搜索推荐
# GloVe词嵌入:全局词频矩阵的高效表示
在自然语言处理(NLP)领域,词嵌入技术作为连接离散文本与连续向量空间的桥梁,已经成为各种文本处理任务的基础。继Word2Vec之后,斯坦福大学在2014年提出的GloVe(Global Vectors for Word Representation)模型为词嵌入技术开辟了新的思路。与Word2Vec专注于局部上下文信息不同,GloVe通过分析词的全局共现统计信息来学习词向量表示,这种方法在捕捉词语间全局语义关系方面具有独特优势。
587 1
|
9月前
|
安全 Linux 网络安全
Metasploit Framework 6.4.88 (macOS, Linux, Windows) - 开源渗透测试框架
Metasploit Framework 6.4.88 (macOS, Linux, Windows) - 开源渗透测试框架
682 0
|
人工智能 搜索推荐 vr&ar
虚拟现实如何改变社交互动?技术加持下的全新社交体验
虚拟现实如何改变社交互动?技术加持下的全新社交体验
490 12
miniconda3彻底删除虚拟环境
这篇文章介绍了如何彻底删除Miniconda3创建的虚拟环境,包括删除环境的命令和步骤。
2640 0
miniconda3彻底删除虚拟环境
|
编解码 小程序 iOS开发
在无影上畅玩《黑神话:悟空》,这十件事你需要知道!
在无影上畅玩《黑神话:悟空》,这十件事你需要知道!
|
缓存 监控 数据库
接口性能飞跃:一次成功的优化实践
在软件开发中,接口性能优化是一个永恒的话题。一个高效的接口不仅能提升用户体验,还能减轻服务器压力,降低运营成本。本文将分享一次成功的接口优化案例,从问题诊断到解决方案实施,详细介绍我们的优化过程。
431 0