在计算机的发展历史中,有这样两大定律Amdahl定律和摩尔定律,这两个定律的更替代表了近年来硬件发展从追求CPU频率到追求多核心并行处理的发展过程。

  • Amdahl定律通过系统并行化与串行化的比重来描述多CPU系统能获得的运算加速能力。
  • 摩尔定律用于描述CPU晶体管数量与运行效率之间的关系。

多任务处理的场景已经是现代计算机必不可少的能力了,这包括多核并行和单核并发。而上层应用程序开发大部分时间都在处理CPU与IO之间的差异性问题,尽可能的去压榨计算机的价值。
在了解Java内存模型之前,需要先知道硬件层面的一些基本知识,为什么会出现一个语言层面的内存模型,Java内存模型定义了什么?解决了什么问题?下面具体分析。

硬件效率与一致性

在计算机硬件快速发展的过程中,达成了一个共识,不同硬件设备之间计算和IO的性能差异是巨大的,那么如何去协调不同硬件之间因性能差异而出现的一致性问题,就需要不同的协议、规范来进行协同。
例如现代计算机上,多任务并行处理的过程中,大部分的时间都浪费在IO的过程中,在IO的时候尽可能的去多利用CPU做其他计算,尽可能的去压榨计算机的能力,当然也不是CPU处理的任务越多越好,过多的线程上下文切换,频繁抢夺资源会导致过多线程阻塞,甚至死锁,这时也会降低程序的并发能力。因此解决硬件之间的效率和一致性问题至关重要。

存储结构层次

计算机组成主要分为五个部分:控制器、运算器、存储器、输入设备、输出设备,其中关于存储器有一个明确的存储层次结构划分:

  • L0: 寄存器
  • L1: L1高速缓存(SRAM)
  • L2: L2高速缓存(SRAM)
  • L3: L3高速缓存(SRAM)
  • L4: 主内存(DRAM)
  • L5: 本地二级存储(本地磁盘)
  • L6: 远程二级存储(分布式文件系统,Web服务器)

在这个存储结构中,从上至下设备访问速度越来越慢,容量越来越大并且每个字节的造价越来越便宜。存储器层次结构的主要目的是上一层的存储器作为低一层存储器的高速缓存。

CPU访问 CPU时钟周期(cycle) 消耗时间(ns)
L0寄存器 1
L1高速缓存 3~4 1
L2高速缓存 10 3
L3高速缓存 40~45 15
QPI总线 20
L4主内存 60~80

关于计算机存储信息,可以使用相关工具查看:

  • windows电脑可以使用工具Coreinfo查看CPU信息
  • Mac & Linux可以使用命令sysctl -a查看存储信息

应用程序在与计算机进行交互时使用最多的就是CPU、内存以及磁盘,由上表中信息可知计算机的存储结构与运算单元的处理速度存在几个数量级的差别,因此可以得出结论,在响应时间方面:CPU >> 内存 >> 磁盘。现代计算机操作系统不得不在CPU和内存之间加入一级或者多级高速缓存作为缓冲,将需要运算的数据复制到缓存,让运算可以快速进行,运算结束后,再从缓存同步到内存,这样CPU无需等待内存读写,实现CPU时间片轮询并发处理任务。
现代计算机中CPU、高速缓存、和内存之间的分布关系示意如下图所示,其中L1、L2高速缓存为每个核心所独有,L3和内存为所有核心共享。
未命名文件.jpg

执行顺序

执行顺序一般包含按序执行(In-Order Execution)和乱序执行(Out-Of-Order Execution)。在说明执行顺序前,这里以统筹方法中,著名数学家华罗庚提到的烧水泡茶举例子:
正常泡茶需要进行一些步骤,但是如果按照顺序一一执行,那消耗的时间就是所有步骤的时间耗费之和,但是烧开水的过程一般比较长,在这个等待期间可以去做一些其他工作。两种工序前者暂时称为传统工序,后者称为统筹工序,两种工序示意图如下所示:
未命名文件.svg

未命名文件 (1).svg
以上说明的是现实生活中顺序和乱序的例子,但是计算机本身也是一门抽象的艺术,源于现实,这些思想反馈的计算机的运算单元就可以划分为按序执行和乱序执行。
在计算机的运算单元中CPU和内存的计算性能差异巨大,因此在CPU和内存之间增加高速缓存,使得CPU被尽可能的应用,除了增加高速缓存之外,CPU可能会对输入代码进行乱序执行优化,在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致,因此如果存在一个计算任务依赖另外一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。与CPU的乱序执行优化类似,Java虚拟机的即时编译器中也有指令重排序(Instruction Reorder)优化。

缓存一致性协议

基于高速缓存很好的解决了CPU与内存速度之间的矛盾,但是在多核CPU场景下也引入了新的问题,那就是缓存一致性问题,每个CPU都有独占的缓存区域(L1,L2)和共享的缓存区域(L3、内存),当多个CPU运算设计到共享区域时可能出现各自缓存数据不一致的情况,那同步会主内存的数据也将出现混乱。为了解决一致性问题,每个处理器在访问缓存时需要遵循某些协议,类似的协议有MSI、MESI、MOSI、Synapse等。常见的intel处理器使用的就是MESI协议。
MESI(Modified Exclusive Shared Or Invalid)(也称为伊利诺斯协议,该协议由伊利诺斯州立大学提出)是一种广泛使用的支持写回策略的缓存一致性协议。
所谓MESI是指CPU中每个缓存行(高速缓存中进行读取时一般是按照块来操作,每次被操作的一片空间被称作缓存行)使用4种状态进行标记(使用额外的两位(bit)表示)。

  • M: 被修改(Modified)

该缓存行只被缓存在该CPU的缓存中,并且是被修改过的(dirty),即与主存中的数据不一致,该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取请主存中相应内存之前)写回(write back)主存。当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。

  • E: 独享的(Exclusive)

该缓存行只被缓存在该CPU的缓存中,它是未被修改过的(clean),与主存中数据一致。该状态可以在任何时刻当有其它CPU读取该内存时变成共享状态(shared)。同样地,当CPU修改该缓存行中内容时,该状态可以变成Modified状态。

  • S: 共享的(Shared)

该状态意味着该缓存行可能被多个CPU缓存,并且各个缓存中的数据与主存数据一致(clean),当有一个CPU修改该缓存行中,其它CPU中该缓存行可以被作废(变成无效状态(Invalid))。

  • I: 无效的(Invalid)

该缓存是无效的(可能有其它CPU修改了该缓存行)。
MESI状态之间的相互转换关系用下表进行表示,表示允许,×表示不允许。

M E S I
M × × ×
E × × ×
S × ×
I

MESI几种状态的转换由不同的事件触发,转换过程如下表所示:

  • Local Read表示本内核读本Cache中的值
  • Local Write表示本内核写本Cache中的值
  • Remote Read表示其它内核读其它Cache中的值
  • Remote Write表示其它内核写其它Cache中的值 | 当前状态 | 事件 | 行为 | 状态转换 | | —- | —- | —- | —- | | I | Local Read | 如果其他Cache没有这份数据,本Cache从该内存中取数据,Cache line状态变成E;
    如果其他Cache有这份数据,且状态为M,则将数据更新到内存,本Cache再从内存中取数据,两个Cache的Cache line状态都变成S;
    如果其他Cache有这份数据,且状态为S或者E,本Cache从内存中取数据,这些Cache的Cache line状态都变成S。 | E/S | | | Local Write | 从内存中取数据,在Cache中修改,状态变成M;
    如果其他Cache有这份数据,切状态为M,需先更新数据到内存;
    如果其他Cache有这份数据,则其他Cache的Cache line状态变成1 | M | | | Remote Read | 既然是Invalid,别的核的操作与它无关 | I | | | Remote Write | | I | | E | Local Read | 从Cache中取数据,状态不变 | E | | | Local Write | 修改Cache的数据,状态为M | M | | | Remote Read | 数据和其他核共用,状态变成了S | S | | | Remote Write | 数据被修改,本Cache line不能再使用,状态变成I | I | | S | Local Read | 从Cache中取数据,状态不变 | S | | | Local Write | 修改Cache中的数据,状态变成M,其他核共享的Cache Line状态变成I | M | | | Remote Read | 状态不变 | S | | | Remote Write | 数据被修改,本CacheLine不能被使用,状态变为I | I | | M | Local Read | 从Cache中取数据状态不变 | M | | | Local Write | 修改Cache中的数据状态不变 | M | | | Remote Read | 这行数据被写到内存中,使其他核能使用到最新的数据,状态变成S | S | | | Remote Write | 这行数据被写到内内存中,使其他核能使用到直数据,由于其它核会修改这行数据,状态变成I | I |

缓存行与伪共享问题

在对高速缓存进行操作的过程中并不是按照单个字节去读取的,为了提高操作效率,一般都是按行读取,高速缓存中每次被读取的一行,称为缓存行,其大小通常为32~256字节,主流处理器的缓存行大小为64字节。如图所示:
未命名文件 (3).svg
由于缓存行存在,当不同的线程在操作两份不同的数据时,如果这两份数据刚好位于同一个缓存行中,那么彼此之间就会互相影响。假设线程1操作数据A,线程2操作数据B,A、B数据位于同一缓存行,当A数据发生修改时,由于缓存一致性协议的规定,就会造成缓存行失效,因此当线程2操作B数据时,尽管线程2之前并没有对B进行过任何操作,也必须重新加载缓存行。同理线程2操作B也会影响着线程1操作A。

通过代码来演示缓存行对齐对伪共享效率提升的作用:

  • 缓存行未对齐

    1. /**
    2. * 测试缓存行未对齐
    3. *
    4. * @author starsray
    5. * @date 2022/06/20
    6. */
    7. public class TestCacheLineNoPadding {
    8. public static class T {
    9. //8字节
    10. private volatile long x = 0L;
    11. }
    12. // 单个缓存行一般是64个字节,arr对象容量为2,占用16个字节,使得位于同一个缓存行
    13. private static final T[] arr = new T[2];
    14. static {
    15. arr[0] = new T();
    16. arr[1] = new T();
    17. }
    18. public static void main(String[] args) throws InterruptedException {
    19. // 两个线程操作,分表操作一个数组中的元素。
    20. // volatile的缓存一致性协议MESI或者锁总线,会消耗时间
    21. Thread thread1 = new Thread(() -> {
    22. for (long i = 0; i < 1000_0000L; i++) {
    23. arr[0].x = i;
    24. }
    25. });
    26. Thread thread2 = new Thread(() -> {
    27. for (long i = 0; i < 1000_0000L; i++) {
    28. arr[1].x = i;
    29. }
    30. });
    31. long startTime = System.nanoTime();
    32. thread1.start();
    33. thread2.start();
    34. thread1.join();
    35. thread2.join();
    36. System.out.println("总计消耗时间:" + (System.nanoTime() - startTime) / 100_000);
    37. }
    38. }
  • 缓存行对齐

    1. /**
    2. * 测试缓存行对齐
    3. *
    4. * @author starsray
    5. * @date 2022/06/20
    6. */
    7. public class TestCacheLinePadding {
    8. private static class Padding {
    9. // 7 * 8字节 构造条件使得数组元素位于两个不同的缓存行进行模拟
    10. public volatile long p1, p2, p3, p4, p5, p6, p7;
    11. }
    12. public static class T extends Padding {
    13. // 8字节
    14. private volatile long x = 0L;
    15. }
    16. private static final T[] arr = new T[2];
    17. static {
    18. arr[0] = new T();
    19. arr[1] = new T();
    20. }
    21. public static void main(String[] args) throws InterruptedException {
    22. Thread thread1 = new Thread(() -> {
    23. for (long i = 0; i < 1000_0000L; i++) {
    24. arr[0].x = i;
    25. }
    26. });
    27. Thread thread2 = new Thread(() -> {
    28. for (long i = 0; i < 1000_0000L; i++) {
    29. arr[1].x = i;
    30. }
    31. });
    32. long startTime = System.nanoTime();
    33. thread1.start();
    34. thread2.start();
    35. thread1.join();
    36. thread2.join();
    37. System.out.println("总计消耗时间:" + (System.nanoTime() - startTime) / 100_000);
    38. }
    39. }

    输出结果

    1. 【缓存行未对齐】总计消耗时间:1905
    2. 【缓存行对齐】总计消耗时间:568

    说明:关于测试案例中,public volatile long p1, p2, p3, p4, p5, p6, p7;在不同版本的Java虚拟机中可能会被识别为无效代码被优化掉,因此没办法来进行填充测试。 JDK8中提供了@Contended注解确保实现填充,该特性为实现性质的,还需要指定JVM参数 -XX:-RestrictContended启用。为了实验准确无误,建议开启。 JDK 8的ConcurrentHashMap源码中,也使用@sun.misc.Contended对静态内部类CounterCell进行修饰。

从实验结果中可以看出,缓存行对齐对于减少MESI总线锁的竞争效果还是比较显著的。Java中一些比较优秀的开源框架也关注了硬件层面对于性能带来的优化,Disruptor,一个开源框架,研发的初衷是为了解决高并发下队列锁的问题,最早由LMAX提出并使用,能够在无锁的情况下实现队列的并发操作,并号称能够在一个线程里每秒处理6百万笔订单。
Java中并发操作相关队列,基本位于juc包,常见的有下面几种方式:

  • ArrayBlockingQueue:基于数组形式的队列,通过加锁的方式,来保证多线程情况下数据的安全;
  • LinkedBlockingQueue:基于链表形式的队列,通过加锁的方式,来保证多线程情况下数据的安全;
  • ConcurrentLinkedQueue:基于链表形式的队列,通过CAS的方式保证数据,线程过多时会过多消耗CPU。

Disruptor底层是基于RingBuffer来实现的,其大概原理是,每个生产者或者消费者线程,会先申请可以操作的元素在数组中的位置,申请到之后,直接在该位置写入或者读取数据,整个过程通过原子变量CAS,保证操作的线程安全,这里不做展开描述。

小结

现代计算机都具备多核并行处理能力,又被称为共享内存多核系统(Shared Memory Mutilprocessors System)。因此在硬件层面出现了各种协议,这些协议是在硬件层面对一致性问题的处理,同样接下来要说明的Java内存模型,目的是解决在多线程并发操作虚拟机内存场景下,对于一致性问题的解决方案,由于Java虚拟机以及其跨平台能力建立在硬件基础之上,了解硬件效率与一致性问题后,就很容易理解Java内存模型建立的目的。

Java内存模型

Java内存模型(Java Memory Model,简称JMM),指的是Java虚拟机规范中试图定义的一种属于Java的内存模型,在JDK1.2开始建立,并在JDK5(JSR-133,原文译文)中成熟完善。
Java内存模型的定义并非一件简单的事情,Java内存模型旨在屏蔽各种硬件和操作系统内存的访问差异,以实现Java程序可以在各种平台都可以达到同一访问效果。从整体特征上来说,Java内存模型围绕的是在并发场景下如何处理原子性、有序性、可见性的特征来建立的,主要解决的还是并发操作,让程序更适合运行在现代计算机的多任务处理系统中。
Java内存模型具体定义了:所有的变量(包括类变量、成员变量,但是不包括方法内局部变量)都应该存储在主内存中;每个线程都有自己的工作内存空间,线程对变量(为主内存变量副本)的读取、赋值操作必须在自己的工作内存进行,不能直接对主内存中的变量值进行操作;不同线程之间无法访问对方工作内存中的变量,线程之间的变量值传递需要经过主内存。

主内存和工作内存

Java内存模型定义了主内存、线程、以及工作内存三者之间的协同关系,如下图所示:
未命名文件 (4).svg
Java内存模型中定义的主内存、工作内存与Java运行时数据区定义的堆、栈、方法区并不是一个维度的划分规则,两种维度之间没有直接对应关系。如果需要一种关系来做相应对应,从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例部分数据(堆中还保存了对象实例的对象头、以及对齐填充等数据),而工作内存对应虚拟机栈中的部分区域。
从另一种角度来看,在JMM中主内存直接对应物理硬件内存,工作内存所处理的数据会优先存储在寄存器和高速缓存中,由于程序运行时主要访问的是工作内存,因此这也是Java虚拟机或者硬件本身的一种速度优化策略。而Java运行时数据区定义的堆、栈等区域,则可以看作对Java虚拟机占有内存空间的一种逻辑划分。

内存间交互

Java内存模型中定义了主内存与工作内存之间的交互方式,明确了一个变量如何从主内存拷贝到工作内存,以及如何从工作内存同步到主内存空间。这些操作细节主要由8种操作来实现,每一种实现都要求是原子性操作,主要如下所示:

  • lock(锁定):作用于主内存变量,把一个变量标识为一个线程独占状态
  • unlock(解锁):作用于主内存变量,把处于锁定状态的变量释放,释放后才可以被其他线程操作
  • read(读取):作用于主内存变量,把变量值从主从读取到工作内存,为load动作做准备
  • load(载入):作用于工作内存变量,把read操作读取到的变量值放入工作内存的变量副本中
  • use(使用):作用于工作内存变量,把该变量值传递给执行引擎(每当虚拟机指令需要使用变量时触发)
  • assign(赋值):作用于工作内存变量,把从执行引擎接收到的值赋值给该变量(每当虚拟机赋值触发动作)
  • store(存储):作用于工作内存变量,把工作内存的变量值传递给主内存,为write操作做准备
  • write(写入):作用于主内存变量,把store操作从工作内存传递过来的值放入主内存变量中

内存交互只是定义了基本的读、写功能,Java内存模型还定义了这些操作之间的组合性、顺序性问题。如:把一个变量从主内存拷贝到工作内存需要经过read和load操作,从工作内存拷贝到主内存需要经过store和write操作,其中read和load,store和write操作只要求顺序,不要求连续,他们执行的过程中间可以允许插入其他指令,例如:read a、read b、load b、load a。Java内存模型还规定了操作时必须满足的规则:

  • 不允许read、load、store、write操作单独出现
  • 不允许线程丢弃最近的assign操作,在工作内存赋值后必须同步写回主内存
  • 不允许线程无原因的把数据从工作内存同步到主内存(未发生过assign操作)
  • 变量只能在主内存中诞生,不允许工作内存直接使用未初始化的变量,use、store之前必须先load、assign
  • 变量在同一时刻只允许一个线程进行lock,lock可以多次,但是unlock需要等同lock的次数
  • 变量执行lock操作,将清空工作内存该变量的值,再次进行load或assign初始化变量的值
  • 未进行lock操作的变量不允许对其进行unlock操作,也不允许unlock被其他线程锁定的变量
  • 变量进行unlock操作前必须先进行store和write操作

Java内存模型中的八种基本操作以及顺序约束,保证的是并发操作下的安全性,这些操作本身过于繁琐,也难于上手,在Java并发编程相关的包java.util.concurrent包将这里的操作简化为read、write、lock、unlock等相关API工具类。

volatile关键字

Java虚拟机在同步方面提供了重量级的同步工具synchronized,尽管在JDK5后进行了锁升级等一系列优化,但是相对于还是较重的。相反,volatile可以说是一种轻量级的同步机制,被volatile关键字修饰的变量具有两种语义:

  • 保证变量内存之间可见性
  • 禁止指令重排序

关于变量内存间可见性指的是当某个线程对变量值进行修改,新值对于其他线程来说可以立即可见的,但是可见并不意味着并发操作的安全性。
volatile的同步机制的性能确实要优于锁(使用synchronized关键字或java.util.concurrent包里面的锁),但是由于虚拟机对锁实行的许多消除和优化,使得很难确切地说volatile就会比synchronized快上多少。如果让volatile与自己比较,那可以确定一个原则:volatile变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢上一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。不过即便如此,大多数场景下volatile的总开销仍然要比锁来得更低。
volatile语义通过内存屏障来禁止指令进行重排序,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略。

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

volatile是通过内存屏障和禁止指令重排序来保证内存可见性的,一个线程对volatile变量的修改,能够立刻被其他线程所见。

特殊类型处理

long和double两种类型占用了8个字节,对于这两种类型,Java虚拟机进行了特殊优化,摘录一段深入Java虚拟机规范中的描述:

Java内存模型要求lock、unlock、read、load、assign、use、store、write这八种操作都具有原子性,但是对于64位的数据类型(long和double),在模型中特别定义了一条宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现自行选择是否要保证64位数据类型的load、store、read和write这四个操作的原子性,这就是所谓的“long和double的非原子性协定”(Non-Atomic Treatment of double and long Variables)。 如果有多个线程共享一个并未声明为volatile的long或double类型的变量,并且同时对它们进行读取和修改操作,那么某些线程可能会读取到一个既不是原值,也不是其他线程修改值的代表了“半个变量”的数值。不过这种读取到“半个变量”的情况是非常罕见的,经过实际测试,在目前主流平台下商用的64位Java虚拟机中并不会出现非原子性访问行为,但是对于32位的Java虚拟机,譬如比较常用的32位x86平台下的HotSpot虚拟机,对long类型的数据确实存在非原子性访问的风险。从JDK 9起,HotSpot增加了一个实验性的参数-XX:+AlwaysAtomicAccesses(这是JEP 188对Java内存模型更新的一部分内容)来约束虚拟机对所有数据类型进行原子性的访问。而针对double类型,由于现代中央处理器中一般都包含专门用于处理浮点数据的浮点运算器(Floating Point Unit,FPU),用来专门处理单、双精度的浮点数据,所以哪怕是32位虚拟机中通常也不会出现非原子性访问的问题,实际测试也证实了这一点。笔者的看法是,在实际开发中,除非该数据有明确可知的线程竞争,否则我们在编写代码时一般不需要因为这个原因刻意把用到的long和double变量专门声明为volatile。

Happens-Before

Java内存模型中的有序性如果都依赖于volatile和synchronized关键字来完成,很多操作会显得代码臃肿。在实际开发中并不是都需要用到这两个关键字,是因为在Java中存在一个先行发生原则(Happens-Before)。这个原则是判断数据是否存在竞争,线程是否安全的重要手段。
先行发生是Java内存模型中定义的两项操作之间的偏序关系,比如说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。
如下面一段测试代码案例,通过volatile来测试先行发生原则:

  1. /**
  2. * TestHappensBefore
  3. *
  4. * @author starsray
  5. * @date 2022/06/20
  6. */
  7. public class TestHappensBefore {
  8. private volatile int i = 1;
  9. private volatile int j = 0;
  10. public static void main(String[] args) throws InterruptedException {
  11. new TestHappensBefore().testHappensBefore();
  12. }
  13. void testHappensBefore() {
  14. Thread t1 = new Thread(() -> System.out.println("t1:" + j));
  15. Thread t2 = new Thread(() -> {
  16. j = i;
  17. System.out.println("t2:" + j);
  18. });
  19. t1.start();
  20. t2.start();
  21. }
  22. }

时间先后顺序与先行发生原则之间基本没有因果关系,衡量并发安全问题的时候不要受时间顺序的干扰,一切必须以先行发生原则为准。虚拟机中已经内置了一些先行发生原则:

  • 程序次序规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。注意,这里说的是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
  • 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是“同一个锁”,而“后面”是指时间上的先后。
  • volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后。
  • 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
  • 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止执行。
  • 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread::interrupted()方法检测到是否有中断发生。
  • 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
  • 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

    总结

  • 计算机硬件设备之间的性能差异引起了效率与一致性的问题,通过一些规范或者协议来解决一致性问题。Java内存模型是在Java虚拟机体系下对有序性、一致性的规范。

  • 对单个CPU来说,乱序执行使得CPU的使用率被尽可能压榨提升,而MESI是解决在多核CPU的多级缓存下与内存中数据的一致性问题。
  • 缓存行是为了提高缓存读取时效率的问题,而伪共享问题则是共享区域在操作时被相互影响导致频繁触发总线锁导致性能下降,缓存行对齐可以优化性能问题,如高性能无锁框架Disruptor就是利用了这一特性。
  • Java内存模型定义了主内存和线程工作内存的交互方式,目的在于解决多线程模式下资源争抢时保证数据安全,happens-before定义了一些优先发生原则,依据这些原则,在需要保证有序性一致性的场景下时无需过度对代码块加锁,减少代码臃肿。

参考文档: