JAVA 多线程与高并发学习笔记(十二)——JMM介绍

本部分介绍Java是如何利用JMM解决并发中的有序性问题的。

由于CPU技术的发展,CPU会优化待执行的指令序列,使指令执行顺序和代码顺序略有不同,可能会导致代码执行出现有序性问题。

内存屏障

内存屏障(Memory Barrier)又称内存栅栏(Memory Fences),是一系列的CPU指令,用来保证特定操作的执行顺序。

重排序

为了提高性能,编译器和CPU常常会对指令进行重排序,包括:

  • 编译器重排序:编译器在不改变单线程程序语义(as-if-serial)的前提下,可以重新安排语句的执行顺序。
  • CPU重排序:为了CPU执行效率,流水线(Pipeline)都是并行处理的,处理次序和程序次序允许不一致。包括两类:
    • 指令集重排序:现在处理器采用指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
    • 内存系统重排序:由于处理器使用了主存和高速缓存,这使得加载和存储操作会乱序执行。

As-if-Serial 规则

As-if-Serial 规则内容为:无论如何重排序,都必须保证代码在单线程下运行正确。为了遵循 As-if-Serial 规则,编译器和CPU不会对存在数据依赖关系的操作进行重排序。

As-if-Serial 规则只能保障单内核指令重排序之后的执行结果正确,不能保障多内核以及跨CPU指令重排序之后的执行结果正确。

硬件层面的内存屏障

内存屏障可以保障跨CPU指令重排序之后的程序结果正确。

硬件层内存屏障的定义

硬件层常用的内存屏障有以下三种:

  • 读屏障(Load Barrier)。在指令前插入读屏障,可以让高速缓存中的数据失效,强制重新从主存加载数据。并且,读屏障会告诉CPU和编译器,先于这个屏障的指令必须先执行。读屏障对应着X86处理器上的ifence指令。
  • 写屏障(Store Barrier)。在指令后插入写屏障指令能让高速缓存中的最新数据更新到主存,让其他线程可见。并且,写屏障会告诉CPU和编译器,后于这个屏障的指令必须后执行。写屏障对应X86处理器上的sfence指令。
  • 全屏障(Full Barrier)。具备读屏障和写屏障的能力,对应X86处理器上的mfence指令。

X86处理器上的lock前缀指令也具有内存全屏障功能。

硬件层的内存屏障的作用

作用有以下两点:

  • 阻止屏障两侧的指令重排序。告诉CPU和编译器先于这个屏障的指令必须先执行,后于这个屏障的指令必须后执行。
  • 强制让高速缓存的数据失效。强制吧高速缓存中的最新数据写回主存,让高速缓存中相应的脏数据失效。

JMM 详解

JMM 介绍

JMM(Java Memory Model,Java内存模型)定义了一组规则和规范,该规范定义了一个线程对共享变量写入时,如何确保对另一个线程是可见的。实际上,JMM提供了合理的禁用缓存以及禁止重排序的方法,所以其核心的价值在于解决可见性和有序性。

JMM 的另一大价值在于能屏蔽各种硬件和操作系统的访问差异,保证Java程序在各种平台下对内存的访问最终都是一致的。

Java内存模型定义了两个概念:

  • 主存:主要存储的是Java实例对象,所有线程创建的实例对象都存放在主存中,无论该实例对象是成员变量还是方法的本地变量,还包括共享的类信息、常量、静态变量。由于是共享数据区域,因此多线程访问同一变量会出现线程安全问题。
  • 工作内存:主要存储当前方法的所有变量信息(工作内存中存储着主存中的变量副本)。线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。

JMM的规定如下:

  1. 所有变量存储在主存中。
  2. 每个线程都有自己的工作内存,且对变量的操作都是在工作内存中进行的。
  3. 不同线程之间无法直接访问彼此工作内存的变量,要想访问你只能通过主存来传递。

在JMM中,Java线程,工作内存、主存之间的关系大致如下图所示。

jmm.png

JMM 与 JVM 物理内存的区别

JMM 属于概念和规范的模型,是一个参考性质的模型。虽然 JVM 也是一个概念和规范维度的模型,但是大家常常将JVM理解为实体的、实现维度的虚拟机,通常是指 HotSpot VM。

Java 代码是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所有管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途。在《Java虚拟机规范(Java SE 8)》中描述了JVM运行时内存区域的结构,如下图所示。

jvm-memory.png

JVM比较复杂,后续计划专门学习,这里就不详细展开了。

JMM只是一种抽象的概念,是一组规则,并不实际存在,不管是工作内存的数据还是主内存的数据,对于计算机硬件来说都会存储在计算机主内存中,当然也有可能存储到CPU缓存或者寄存器中,因此总体上来说,Java内存模型和计算机硬件内存架构是一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。

jmm2.jpg

JMM 的8个操作

JMM 定义了一套自己的主存和工作内存之间的交互协议,其中包含8种操作,并且要求JVM具体实现必须保证都是原子的。具体如下表:

操作 作用对象 说明
Read(读取) 主存 Read操作把一个变量的值从主存传输到工作内存中
Load(载入) 工作内存 Load操作把Read从主存中得到的变量载入工作内存的变量副本中
Use(使用) 工作内存 Use操作把工作内存中的一个变量的值传递给执行引擎
Assign(赋值) 工作内存 执行引擎通过Assign操作给工作内存变量赋值
Store(存储) 工作内存 把工作内存中的一个变量的值传递到主存中
Write(写入) 主存 把Store操作从工作内存中得到的变量值放入主存的变量中
Lock(锁定) 主存 把一个变量标识为某个线程独占状态
Unlock(解锁) 主存 把一个处于锁定状态的变量释放出来,释放后可以被其它线程锁定

8个操作之间的关系可以参考下图。

jmm-operations.png

JMM 还规定了执行上述8中操作时必须满足如下规则:

  1. 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。

  2. 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。

  3. 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。

  4. 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign和load操作。

  5. 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。

  6. 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值

  7. 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。

  8. 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。

JMM如何解决有序性问题

JMM 提供了自己的内存屏障指令,要求JVM编译器实现这些指令。

JMM 内存屏障主要有 Load 和 Store 两类,具体如下:

  1. Load Barrier(读屏障)。在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主存家在数据。
  2. Store Barrier(写屏障)。在写指令之后插入写屏障,能让写入缓存的最新数据写回主存。

在实际使用时,会对以上两类屏障两两组合:

  1. LoadLoad(LL)屏障。在执行预加载(或支持乱序处理)的指令序列中,通常需要显式地声明LoadLoad屏障,因为这些Load指令可能会依赖其他CPU执行的Load指令的结果。

  2. StoreStore(SS)屏障。通常情况下,如果CPU不能保证从高速缓存向主存或其它CPU按顺序刷新数据,那么它需要使用StoreStore屏障。

  3. LoadStore(LS)屏障。该屏障用于在数据写入操作执行前确保完成数据的读取。

  4. StoreLoad(SL)屏障。该屏障用于在数据读取操作执行前,确保完成数据的写入。该屏障开销最大,但它是一个全能型屏障。

volatile 语义中的内存屏障

Java 代码中,volatile 关键字主要有两层语义:

  • 不同线程对 volatile 变量的值具有内存可见性,即一个线程修改了某个 volatile 变量的值,该值对其它线程立即可见。
  • 禁止进行指令重排序。

基于保守策略的 volatile 操作的内存屏障插入策略:

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

这是JMM建议的策略,不同的处理器有不同的“松紧度”的处理器内存模型。

Happens-Before规则

JMM定义了 Happens-Before(先行发生)规则,并且确保只要两个Java语句之间必须存在Happens-Before关系,JMM尽量确保这两个Java语句之间的内存可见性和指令有序性。

Happens-Before 规则主要包括:

  1. 程序顺序执行规则(as-if-serial规则)。在同一个线程中,有依赖关系的操作按照先后顺序,前一个操作必须先行发生于后一个操作。
  2. volatile变量规则。对volatile变量的写操作必须先行发生于对volatile变量的读操作。
  3. 传递性规则。如果A操作先于B操作,而B操作又先行于C操作,那么A操作先于C操作。
  4. 监视锁规则(Monitor Lock Rule)。对一个监视锁的解锁操作先行发生于后续对这个监视锁的加锁操作。
  5. start规则。对线程的start操作先行于这个线程内部的其它任何操作。
  6. join规则。如果线程A执行了B.join()操作并成功返回,那么线程B中的任意操作先行发生于线程A所执行的B.join()操作。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353

推荐阅读更多精彩内容