「多线程基础」Java内存模型详解

JMM(Java Memory Model)即Java内存模型,更多体现为一种规范和规则。该规范定义了一个线程共享变量的写入时,如何确保对另一个线程可见的

JMM虽然并不像JVM内存结构一样是真实存在的运行实体,但其具备以下价值:

  • 通过合理的禁止缓存和重排序的方法,解决了可见性和有序性问题(核心价值);
  • 屏蔽了各种硬件和操作系统的访问差异,保证了Java程序在各种平台下对内存的访问是最终一致的。

Java内存模型中的两个概念

  1. 主内存:主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的局部变量。当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发生线程安全问题
  2. 工作内存:主要存储当前方法的所有本地变量信息(工作内存中存储着主内存的变量副本),每个线程只能访问自己的工作内存,即线程中的本地变量对其他线程是不可见的,就算是两个线程执行同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,包括字节码行号指示器、相关Native方法信息。由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题


Java内存模型规定

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


「多线程基础」Java内存模型详解


JMM将所有的变量都存放在公共主内存中,当线程使用变量时,会把公共主内存里的变量复制到自己的私有工作内存中,线程对变量的读写操作,实际操作的是自己工作内存中的变量副本。因此JMM模型也需要解决代码重排序和缓存可见性问题。如何解决呢,JMM提供的方案包括volatile、synchronized、final等等。JMM定义了一些内存操作的抽象指令集,然后将这些抽象指令包含到volatile、synchronized等关键字的语义中,并要求JVM在实现这些关键字时必须具备其包含的JMM抽象指令的能力

JMM与JVM运行时内存的关系

JMM是属于语言级别的内存模型,它确保了在不同的编译器不同的处理平台上,为Java程序员提供一致的内存可见性保证指令并发执行的有序性。JMM属于概念和规范维度的模型,是一个参考性质的模型,定义了一个指令集、一个虚拟计算架构、一个执行模型。具体的JVM需要遵循JMM的定义进行实现,它能够运行根据JMM模型指令集编写的代码,就像真机可以运行机器代码一样。


JVM是Java虚拟机,虽然也是一个概念和规范维度的模型,但通常将JVM理解为实体的、实现维度的虚拟机,一把指HotSpot VM。JVM在执行Java程序时,会把所管理的内存划分为若干个不同的数据区域,每个区域用途不同,有些区域随着虚拟机进程的启动而存在,有些区域依赖用户线程的启动和结束而建立和销毁。《Java虚拟机规范(Java SE 8)》中描述的JVM运行时内存区域结构如图:

「多线程基础」Java内存模型详解


JMM与硬件内存架构的关系

多线程的执行最终都会映射到硬件处理器上执行,但是JMM与硬件内存架构并不完全一致。对于硬件来说,只有寄存器、高速缓存、主存的概念,并没有工作内存(线程私有数据区)和主内存(堆内存)之分,也就是说JMM对内存的划分对硬件内存没有任何影响,因为JMM只是一种抽象的概念,是一组规则,并不实际存在。无论是JMM工作内存还是主内存的数据,对于计算机硬件来说都会存储在计算机主存中,也可能存储到CPU高速缓存、或者寄存器中。总体来说,JMM和计算机硬件内存架构是一个相互交叉的关系,是一种抽象概念的划分真实物理硬件的交叉


「多线程基础」Java内存模型详解


Java内存模型的8个操作

JMM定义了一套自己的主内存与工作内存之间的交互协议,即一个变量如何从主内存拷贝到工作内存,又是如何从工作内存写入主内存。该协议有8中操作,要求JVM在实现时必须保证其中每一个操作都是原子的。这8种操作是:

  • Read:作用于主内存的变量。把一个变量的值从主内存传输到工作内存中,以便随后的Load操作。
  • Load:作用于工作内存的变量。将Read操作从主内存中得到的变量值,载入工作内存的变量副本中。变量副本可以理解为CPU的高速缓存。
  • Use:作用于工作内存的变量。将工作内存中的一个变量的值传递给执行引擎。
  • Assign:作用于工作内存的变量。执行引擎使用该操作给工作内存中的变量赋值。
  • Store:作用于工作内存的变量。将工作内存中的一个变量的值传递到主内存中,以便随后的Write操作。
  • Write:作用于主内存的变量。将Store操作从工作内存中得到的变量值,放入主内存的变量中
  • Lock:作用于主内存的变量。将一个变量标识为某个线程独占状态
  • UnLock:作用于主内存的变量。将一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定


「多线程基础」Java内存模型详解


简单来说,把一个变量从主内存拷贝到工作内存,就要按顺序执行Read和Load操作;线程通过Use和Assign操作对变量进行计算处理;如果要把变量从工作内存同步会主内存,就要按顺序执行Store和Write操作。

注意:JMM要求Read和Load、Store和Write不能单独出现,其必须按顺序执行,但不要求是连续执行,即在Read和Load之间、Store和Write之间可以插入其他指令

以上JMM的8个操作规范定义相当严谨,也极为繁琐,JVM实现起来也非常复杂。Java设计团队在新的JMM版本中不断地对这些操作进行简化,如将8个操作简化为Read、Write、Lock、UnLock四个操作。虽然进行了简化,但是JMM的基础设计并未改变。

Java内存模型的内存屏障

JMM提供了自己的内存屏障指令,要求JVM编译器实现这些指令,禁止特定类型的编译器和处理器重排序,从而解决了顺序一致性问题。

JMM定义了不对应任何处理器的JMM逻辑层内存屏障,来屏蔽底层CPU硬件平台的差异。主要有Load和Store两类:

  • Load Barrier-读屏障:在读指令前插入屏障,可以让高速缓存中的数据失效,重新从主内存中加载数据。
  • Store Barrier-写屏障:在写指令前插入屏障,能让高速缓存中的数据写回到主内存中。

在实际使用中,对Load Barrier和Store Barrier两类屏障进行组合,组合成四个屏障,用于禁止特定类型的处理器重排序:

  • LoadLoad屏障:在执行预加载的指令序列中,通常需要显示声明LL屏障,因为这些Load指令可能会依赖其他处理器执行的Load指令的结果。一段使用LoadLoad屏障的伪代码
Load1;LoadLoad;Load2
该示例的含义为:
在Load2要读取的数据被访问前,使用LoadLoad屏障保证Load1要读取的数据被读取完毕。

该指令会让【编译器】和【处理器】禁止对其前后的指令进行重排序;
该指令会让高速缓存中的数据失效,重新从主内存加载数据
  • StoreStore屏障:如果处理器不能保证从高速缓存想主存按顺序刷新数据,就需要StoreStore凭证。伪代码如下:
Store1;StoreStore;Store2;
该示例的含义为:
在Store2及后续写入操作执行前,使用StoreStore屏障保证Store1的写入结果对其他处理器可见。

该指令会让【编译器】和【处理器】禁止对其前后的指令进行重排序;
该指令会让高速缓存从的最新数据写回到主内存;
  • LoadStore屏障:用于在数据写入操作执行前,确保完成数据的读取。伪代码如下:
Load1;LoadStore;Store2;
该示例的含义为:
在Store2及后续写入操作执行前,使用LoadStore屏障保证Load1要读取的数据被读取完毕。

该指令会让【编译器】和【处理器】禁止对其前后的指令进行重排序;
该指令会让高速缓存中的数据失效,重新从主内存加载数据;
  • StoreLoad屏障:用于在数据读取操作执行前,确保完成数据的写入。伪代码如下:
Store1;StoreLoad;Load2
该示例的含义为:
在Load2及后续所有读取操作执行前,使用StoreLoad屏障保证Store1的写入结果对所有处理器可见。

该指令会让【编译器】和【处理器】禁止对其前后的指令进行重排序;
该指令会让高速缓冲中Store1指令结果【写回主内存】;
该指令会让高速缓存中Load2执行重新从主内存加载数据

JMM中的四个内存屏障的性能开销是不一样的。

LoadLoad、StoreStore两个屏障的性能最高。因为在这两个屏障的上下文中,高速缓存和主内存只需要一种类型的交互即可完成,JMM只需保证同类型交互的先后顺序即可,缓存数据一致性的维护工作量相对较小LoadStore的性能相对较低。此屏障要求数据加载执行在前,数据写入执行在后,只要求了被加载数据的可见性,没有要求后面写入数据的可见性,所以,该屏障没有缓存数据一致性的维护工作量。另外,该屏障限制了Store不能重排到Load之前。

StoreLoad的性能最低,原因是:需要维护缓存数据一致性。

对应到物理硬件平台上,JMM的StoreLoad屏障,最终会编译成硬件层面的全屏障(Full Barrier)。而全屏障不仅仅要让寄存器、高速缓存中的最新数据写回到主内存,还要让高速缓存中的数据失效,重新从主内存加载数据,另外,还全方位的禁止了对屏障指令前后的Store/Load指令进行重排序。所以StoreLoad屏障的性能最低、开销最大。

Java内存模型中的Happens-Before规则

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

Happens-Before规则主要包括一下几个方面:

  1. 程序顺序执行规则(As-If-Serial规则):在同一个线程里面,有依赖关系的操作按照先后顺序,前一个操作必须先行于(happens-before)后一个操作;换句话说,单线程中的代码顺序不管如何重排,结果都不变;
  2. volatile变量规则:对volatile修饰变量的写操作,必须先行于(happens-before)对volatile变量的读操作。
  3. 传递性规则:如果A操作先行于B操作,而B操作先行于C操作,那么A操作先行于(happens-before)C操作。
  4. 监视锁规则:对于一个监视锁的解锁操作,先行于(happens-before)后续对这个监视锁的加锁操作。
  5. start规则:如果线程A执行B.start()启动线程B,那么线程A的B.start()操作先行于(happens-before)线程B中的任何操作。
  6. join规则:如果线程A执行了B.join()操作并成功返回,那么线程B中的任何操作先行于(happens-before)线程A所执行的B.join()操作。
发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章