Java内存模型

硬件效率与一致性

计算机的存储设备处理器的运算速度几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存来作为内存处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存中将数据同步回内存之中

基于高速缓存的存储交互很好的解决了处理器与内存的速度矛盾,但也引入了一个新的问题:缓存一致性。在多处理器系统中,每个处理器都有自己的高速缓存,又共享同一主内存,多个处理器的运算任务都涉及同一块主内存区域时,可能导致各自的缓存数据不一致

为了解决缓存数据一致性问题,需要各个处理器访问缓存时都遵循一些协议,如:MSIMESIMOSISynapseFireflyDragon Protocol等。

处理器、高速缓存、主内存间的交互关系

除了增加高速缓存外,为了使处理器内部的运算单元尽可能被充分利用,处理器可能会对输入的代码进行乱序执行优化,即指令重排序,处理器会在计算之后将乱序执行的结果重组保证该结果与顺序执行的结果是一致的,但不保证程序中各个语句计算的先后顺序与输入的代码中的顺序一致。

Java虚拟机的即时编译器中也有类似的指令重排序优化

JVM-JMM-CPU底层全执行流程

MESI协议

MESI协议是通过锁缓存行,一个缓存行大小为64byte,当超过该值MESI协议将失效,这时会升级成锁总线,每个Cache line有4个状态,可用2个bit表示,MESI四个字母表示这四种状态:

状态 描述 监听任务
M 修改 (Modified) 缓存行有效,数据被修改和内存中的数据不一致,数据只存在于本Cache中。 缓存行须监听所有试图读该缓存行相对主存的操作,该操作必须在缓存将该缓存行写回主存前,并将状态置为S之前被延迟执行。
E 独享、互斥(Exclusive) 缓存行有效,数据和内存中的数据一致,数据只存在于本Cache中。 缓存行须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S状态。
S 共享 (Shared) 缓存行有效,数据和内存中的数据一致,数据存在于很多Cache中。 缓存行须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效。
I 无效 (Invalid) 缓存行无效

对于M和E状态是精确的,在和该缓存行的真正状态是一致的,而S状态可能是非一致的。若缓存将处于S状态的缓存行作废,而另一个缓存实际上可能已经独享了该缓存行,但该缓存却不会将该缓存行升迁为E状态,因为其它缓存不会广播作废掉该缓存行的通知,同样由于缓存并没有保存该缓存行的copy的数量,因此没有办法确定自己是否已经独享了该缓存行。

MESI协议状态切换过程

缓存行伪共享

目前主流的CPU Cache的Cache Line大小都是64Bytes。多线程情况下若修改共享同一个缓存行的变量,就会无意中影响彼此的性能。如2个long 型变量 a 、b,若t1访问a,t2访问b,而a与b刚好在同一个cache line中,此时t1先修改a,将导致b被刷新。

为了解决伪共享,Java8新增了sun.misc.Contended注解,该注解的类会自动补齐缓存行,此注解默认是无效的,需在jvm启动时设置-XX:-RestrictContended参数。

Store Bufferes

为了避免这种CPU运算能力浪费,处理器把它想要写入主存的值写到Store Bufferes缓存,然后继续去处理其他事情,当所有失效确认都接收到时,数据才会最终被提交。

Java内存模型

主内存与工作内存

Java虚拟机中试图定义一种Java内存模型JMM屏蔽各种硬件操作系统内存访问差异C/C++语言是直接使用物理硬件和操作系统的内存模型,由于不同平台上内存模型的差异,可能导致不同平台上并发访问出错。

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存从内存中取出变量这样的底层细节。这里说的变量包括:实例字段静态字段构成数组对象的元素,不包括:局部变量方法参数。因为局部变量和方法参数是线程私有的。

Java内存模型并没有限制执行引擎使用处理器特定寄存器缓存来和主内存交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。

Java内存模型规定了所有的变量都存储在主内存(虚拟机内存中的一部分)中,每条线程有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行不能直接读写主内存中的变量。不同的线程间也无法直接访问对方的工作内存中的变量,线程间变量值得传递均需要通过主内存来完成。

注:拷贝副本,事实上并不会完全复制一份对象拷贝出来,该对象的引用对象中某个在线程访问到的字段是有可能存在拷贝的,但不会有虚拟机实现成把整个对象拷贝一次

线程、主内存、工作内存三者交互关系

注:若局部变量是一个reference类型,它引用的对象在堆中可被各个线程共享,但reference本身在Java栈的局部变量表中,是线程私有的。

内存间交互操作

Java内存模型中定义了以下8种操作来完成主内存工作内存之间的具体交互协议,虚拟机实现时必须保证每一种操作都是原子的、不可再分的。

操作 作用域 完成的工作
lock锁定 内存变量 把一个变量标识为一条线程独占状态
unlock解锁 内存变量 把一个处于锁定状态的变量释放出来,释放后才可被其他线程锁定
read读取 内存变量 把一个变量的值从主内存传输到线程的工作内存
load载入 工作内存变量 read操作从主内存中得到的变量值放入工作内存的变量副本
use使用 工作内存变量 把工作内存中变量值传递给执行引擎
assign赋值 工作内存变量 把从执行引擎接收到的值赋给工作内存的变量
store存储 工作内存变量 把工作内存中一个变量的值传递到主内存中
write写入 内存变量 store操作从工作内存中得到的变量值放入主内存的变量中

若把一个变量从主内存复制到工作内存,需要顺序执行readload操作。若要将变量从工作内存同步回主内存,需顺序执行storewrite操作。Java内存模型只要求上述两个操作必须顺序执行,而没有保证是连续执行

Java内存模型规定了在执行上述8种操作时必须满足以下规则:

  • 不允许readloadstorewrite操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或从工作内存发起回写但主内存不接受的情况出现。
  • 不允许线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把它同步回主内存
  • 不允许线程无原因地(未发生任何assign操作)把数据从线程工作内存同步回主内存
  • 一个新变量只能在主内存中诞生不允许工作内存中直接使用一个未被初始化loadassign)的变量。即对一个变量实施usestore操作之前,必须先执行过了assignload操作。
  • 一个变量同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数unlock操作,变量才会被解锁。
  • 若对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量钱,需要重新执行loadassign操作初始化变量的值。
  • 若变量事先没有被lock操作锁定,不允许对它执行unlock操作,也不允许unlock一个被其他线程锁定的变量。
  • 对变量执行unlock操作之前,必须先把此变量同步回主内存中

happens-before原则

从JDK 5开始Java使用新的JSR-133内存模型,提供了happens-before原则辅助保证程序执行的原子性、可见性以及有序性问题,它是判断数据是否存在竞争、线程是否安全的依据,happens-before原则内容如下:

  • 程序顺序原则:即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
  • 锁规则:解锁unlock操作必发生在后续同一个锁加锁lock前,若同一个锁解锁后,再加锁,则加锁动作必须在解锁动作之后
  • volatile规则volatile变量的写,先发生于读,这保证了volatile变量的可见性,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
  • 线程启动规则线程的start()方法先于它的每一个动作,即若线程A在执行线程B的start方法之前修改了共享变量的值,则当线程B执行start方法时,线程A对共享变量的修改对线程B可见
  • 传递性:A先于B ,B先于C则A必然先于C
  • 线程终止规则线程的所有操作先于线程的终结,Thread.join()方法作用是等待当前执行的线程终止。若在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可通过Thread.interrupted()方法检测线程是否中断
  • 对象终结规则:对象的构造函数执行结束先于finalize()方法