继续创造,加快成长!这是我参加「日新方案 10 月更文挑战」的第31天,点击检查活动详情

前语

Java 言语在设计之初就引入了线程的概念,以充分使用现代处理器的核算才能,这既带来了强大、灵敏的多线程机制,也带来了线程安全等令人混淆的问题,而 Java 内存模型(Java Memory Model,JMM)为咱们供给了一个在纷乱之中达到共同的辅导原则。

本篇博文的重点是,Java 内存模型中的 happen-before 是什么?

概述

Happen-before 联系,是 Java 内存模型中确保多线程操作可见性的机制,也是对前期言语标准中含糊的可见性概念的一个精确界说。

它的详细体现形式,包括但远不止是咱们直觉中的 synchronized、volatile、lock 操作次序等方面,例如:

  • 线程内履行的每个操作,都确保 happen-before 后面的操作,这就确保了根本的程序次序规矩,这是开发者在书写程序时的根本约好。
  • 关于 volatile 变量,对它的写操作,确保 happen-before 在随后对该变量的读取操作。
  • 关于一个锁的解锁操作,确保 happen-before 加锁操作。
  • 方针构建完成,确保 happen-before 于 finalizer 的开端动作。
  • 甚至是相似线程内部操作的完成,确保 happen-before 其他 Thread.join() 的线程等。

这些 happen-before 联系是存在着传递性的,假如满意 a happen-before b 和 b happen-before c,那么 a happen-before c 也成立。

前面我一向用 happen-before,而不是简略说前后,是因为它不仅仅是对履行时间的确保,也包括对内存读、写操作次序的确保。仅仅是时钟次序上的先后,并不能确保线程交互的可见性。

正文

为什么需求 JMM,它试图处理什么问题?

Java 是最早尝试供给内存模型的言语,这是简化多线程编程、确保程序可移植性的一个腾跃。前期相似 C、C++ 等言语,并不存在内存模型的概念(C++ 11 中也引入了标准内存模型),其行为依赖于处理器自身的内存共同性模型,但不同的处理器或许差异很大,所以一段 C++ 程序在处理器 A 上运转正常,并不能确保其在处理器 B 上也是共同的。

即使如此,开端的 Java 言语标准仍然是存在着缺点的,其时的方针是,希望 Java 程序可以充分使用现代硬件的核算才能,一起坚持“书写一次,处处履行”的才能。

可是,显然问题的杂乱度被低估了,跟着 Java 被运转在越来越多的平台上,人们发现,过于泛泛的内存模型界说,存在许多模棱两可之处,对 synchronized 或 volatile 等,相似指令重排序时的行为,并没有供给明晰标准。这儿说的指令重排序,既可所以编译器优化行为,也或许是源自于现代处理器的乱序履行等。

换句话说:

  • 既不能确保一些多线程程序的正确性,例如最著名的就是双检锁(Double-Checked Locking,DCL)的失效问题,双检锁或许导致未完整初始化的方针被访问,理论上这叫并发编程中的安全发布(Safe Publication)失利。
  • 也不能确保同一段程序在不同的处理器架构上体现共同,例如有的处理器支撑缓存共同性,有的不支撑,各自都有自己的内存排序模型。

所以,Java 迫切需求一个完善的 JMM,可以让一般 Java 开发者和编译器、JVM 工程师,可以明晰地达到共同。换句话说,可以相对简略并精确地判别出,多线程程序什么样的履行序列是契合标准的。

所以:

  • 关于编译器、JVM 开发者,关注点或许是怎么运用相似内存屏障(Memory-Barrier)之类技能,确保履行成果契合 JMM 的推断。
  • 关于 Java 使用开发者,则或许愈加关注 volatile、synchronized 等语义,怎么使用相似 happen-before 的规矩,写出可靠的多线程使用,而不是使用一些“秘籍”去糊弄编译器、JVM。

我画了一个简略的人物层次图,不同工程师分工合作,其实所处的层面是有差异的。JMM 为 Java 工程师隔离了不同处理器内存排序的差异,这也是为什么我通常不主张过早深化处理器体系结构,某种意义上来说,这样本就违反了 JMM 的初衷。

【JAVA】Java 内存模型中的 happen-before

JMM 是怎么处理可见性等问题的呢?

在这儿有必要简要介绍一下典型的问题场景。

在 【JAVA】JVM 内存区域的划分 里介绍了 JVM 内部的运转时数据区,可是真正程序履行,实践是要跑在详细的处理器内核上。你可以简略理解为,把本地变量等数据从内存加载到缓存、寄存器,然后运算完毕写回主内存。你可以从下面示意图,看这两种模型的对应。

【JAVA】Java 内存模型中的 happen-before

看上去很夸姣,可是当多线程同享变量时,状况就杂乱了。试想,假如处理器对某个同享变量进行了修正,或许只是体现在该内核的缓存里,这是个本地状况,而运转在其他内核上的线程,或许仍是加载的旧状况,这很或许导致共同性的问题。从理论上来说,多线程同享引入了杂乱的数据依赖性,不论编译器、处理器怎么做重排序,都必须尊重数据依赖性的要求,否则就打破了正确性!这就是 JMM 所要处理的问题。

JMM 内部的完成通常是依赖于所谓的内存屏障,经过禁止某些重排序的方法,供给内存可见性确保,也就是完成了各种 happen-before 规矩。与此一起,更多杂乱度在于,需求尽量确保各种编译器、各种体系结构的处理器,都可以供给共同的行为。

我以 volatile 为例,看看怎么使用内存屏障完成 JMM 界说的可见性?

关于一个 volatile 变量:

  • 对该变量的写操作之后,编译器会刺进一个写屏障
  • 对该变量的读操作之前,编译器会刺进一个读屏障

内存屏障可以在相似变量读、写操作之后,确保其他线程对 volatile 变量的修正对其时线程可见,或许本地修正对其他线程供给可见性。换句话说,线程写入,写屏障会经过相似强迫刷出处理器缓存的方法,让其他线程可以拿到最新数值。

假如你对更多内存屏障的细节感兴趣,或许想了解不同体系结构的处理器模型,主张参阅 JSR-133 相关文档,我个人认为这些都是和特定硬件相关的,内存屏障之类只是完成 JMM 标准的技能手段,并不是标准的要求。

从使用开发者的视点,JMM 供给的可见性,体现在相似 volatile 上,详细行为是什么样呢?

我这儿循序渐进的举两个例子。

首要,请看下面的代码片段,希望达到的效果是,当 condition 被赋值为 false 时,线程 A 可以从循环中退出。

// Thread A
while (condition) {
}
// Thread B
condition = false;

这儿就需求 condition 被界说为 volatile 变量,否则其数值改变,往往并不能被线程 A 感知,进而无法退出。当然,也可以在 while 中,增加可以直接或直接起到相似效果的代码。

第二,我想举 Brian Goetz 供给的一个经典用例,运用 volatile 作为护卫方针,完成某种程度上轻量级的同步,请看代码片段:

Map configOptions;
char[] configText;
volatile boolean initialized = false;
// Thread A
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;
// Thread B
while (!initialized)
  sleep();
// use configOptions

JSR-133 从头界说的 JMM 模型,可以确保线程 B 获取的 configOptions 是更新后的数值。

也就是说 volatile 变量的可见性发生了增强,可以起到看护其上下文的效果。线程 A 对 volatile 变量的赋值,会强制将该变量自己和其时其他变量的状况都刷出缓存,为线程 B 供给可见性。当然,这也是以必定的性能开支作为价值的,但毕竟带来了愈加简略的多线程行为。

咱们经常会说 volatile 比 synchronized 之类愈加轻量,但轻量也仅仅是相对的,volatile 的读、写仍然要比一般的读写要开支更大,所以假如你是在性能高度敏感的场景,除非你确认需求它的语义,否则慎用。

后记

以上就是【JAVA】Java 内存模型中的 happen-before的所有内容了;

从 happen-before 联系开端,帮你理解了什么是 Java 内存模型。为了更方便理解,我作了简化,从不同工程师的人物划分等视点,论述了问题的由来,以及 JMM 是怎么经过相似内存屏障等技能完成的。最后,我以 volatile 为例,分析了可见性在多线程场景中的典型用例。

上篇精讲:【JAVA】Java 常见的垃圾收集器有哪些?

我是,期待你的关注;

创造不易,请多多支撑;

系列专栏:面试精讲 JAVA