Java内存模型与线程

TPS:衡量一个服务性能的高低好坏,每秒事务处理数(Transactions Per Second,TPS),是最重要的指标之一,代表着一秒内服务端能响应的请求总数。

绝大多数的运算任务不可能只靠处理器“计算”完成,处理器至少要与内存交互,如读取数据、存储运算结果等,这个I/O操作是很难消除的,而计算机存储设备与处理器的运算速度有几个数量级的差距,因此要加入一层读写速度尽可能接近处理器运算速度的高速缓存Cache来作为内存和处理器之间的缓冲,这样处理器就无须等待缓慢的内存读写了。

Cache的引入虽然很好地解决了处理器与内存的速度矛盾,但也带来了新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而它们有共享同一主内存(Main Memory),如下图所示。当多个处理器的运算任务都涉及到同一块主内存区域时,可能导致各自的缓存数据不一致,那么同步到主内存时以谁的缓存数据为准?为了解决一致性的问题,就需要各个处理器访问缓存时遵循一些协议,如MSI、MESI、MOSI等等。

所谓“内存模型”,就是指在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。不同的物理机器拥有不一样的内存模型,Java虚拟机也有自己的内存模型。

除了增加高速缓存外,为了使处理器内部的运算单元尽可能被充分利用,处理器可能对输入代码进行乱序执行优化,计算之后再重组乱序执行的结果,Java虚拟机的即时编译器也有类似的指令重排序优化。

12.3 Java内存模型

Java虚拟机试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

1. 主内存与工作内存

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存或从内存中取出变量这样的底层细节。这里的变量(Variables)与Java编程中所说的变量所有区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量和方法参数,因为后者是线程私有的,不会被共享,不存在竞争问题(PS:如果局部变量是reference类型,它引用的对象在Java堆中可被各个线程共享,但是reference本身是在Java栈的局部变量表中,它是线程私有的)。Java内存模型没有限制特定寄存器或缓存来与主内存交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。

Java内存模型规定了所有的变量都存储在主内存(Main Memory)中,每条线程还有自己的工作内存(Working Memory),线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。线程、主内存、工作内存三者的交互关系图如下,可与上图处理器、高速缓存、主内存三者的关系对比:

这里所讲的主内存、工作内存与先前所讲的Java内存区域中的Java堆、栈、方法区等不是同一个层次的内存划分,两者没有关系。但是非要对应起来的话,主内存对应于Java堆中的对象实例数据部分,而工作内存对应于虚拟机栈中的部分数据。从更低层次上说,主内存对应于物理硬件内存,而工作内存可能对应于寄存器和高速缓存,因为程序运行时主要访问读写的是工作内存。

2. 内存间交互操作

主内存与工作内存间的交互,即变量从主内存拷贝到工作内存,从工作内存同步回主内存等,Java内存模型定义了以下8中操作来完成,虚拟机实现时必须保证下面提及的每一个操作都是原子的、不可再分的(double、long类型除外):

(1) lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占的状态。

(2) unclock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

(3) read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存,以便随后的load动作使用。

(4) load(载入):作用于工作内存的变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中。

(5) use(使用):作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎。

(6) assign(赋值):作用于工作内存的变量,把执行引擎接收到的值赋给工作内存的变量。

(7) store(存储):作用于工作内存的变量,把工作内存中一个变量的值传送给主内存中,以便随后的write操作使用。

(8) write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中。

如果要把一个变量从主内存复制到工作内存,就要顺序执行read和load操作;

如果要把一个变量从工作内存同步回主内存,就要顺序执行store和write操作。

对于以上8种操作的规定 :

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

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

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

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

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

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

(7) 如果对一个变量执行lock操作,那么会清空工作内存次变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。

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

3. 对于volatile型变量的特殊规则

当一个变量定义为volatile后,它具备两种特性:

(1) 特性一:保证此变量对所有线程的可见性,指当一条线程修改了这个变量的值,新值对于其他线程是立即得知的,而普通变量在线程间传递需要借助主内存完成;

PS:但是,这不能说明基于volatile变量的运算在并发下就是安全的。由于volatile变量只能保证可见性,除了满足以下两个条件的场景可以使用volatile关键字之外,我们还是要通过加锁(synchronized或者java.util.concurrent中的原子类)来保证原子性。

条件一:运算结果不依赖变量的当前值,或确保只有单一的线程修改变量值。

条件二:变量不需要与其他的状态变量共同参与不变约束。

(2) 特性二:禁止指令重排序优化。普通变量只能保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确结果,但不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。(指令重排序会干扰程序的并发执行)

Java内存模型对volatile变量定义的特殊规则:

假定T表示一个线程,V和W分别表示两个volatile变量,那么在进行read、load、use、assign、store、write操作时需要满足如下的规则:

(1) 只有当线程T对变量V执行的前一个动作是load的时候,线程T才能对变量V执行use动作;并且,只有当线程T对变量V执行的后一个动作是use的时候,线程T才能对变量V执行load操作。线程T对变量V的use操作可以认为是与线程T对变量V的load和read操作相关联的,必须一起连续出现。这条规则要求在工作内存中,每次使用变量V之前都必须先从主内存刷新最新值,用于保证能看到其它线程对变量V所作的修改后的值。

(2) 只有当线程T对变量V执行的前一个动是assign的时候,线程T才能对变量V执行store操作;并且,只有当线程T对变量V执行的后一个动作是store操作的时候,线程T才能对变量V执行assign操作。线程T对变量V的assign操作可以认为是与线程T对变量V的store和write操作相关联的,必须一起连续出现。这一条规则要求在工作内存中,每次修改V后都必须立即同步回主内存中,用于保证其它线程可以看到自己对变量V的修改。

(3) 假定操作A是线程T对变量V实施的use或assign动作,假定操作F是操作A相关联的load或store操作,假定操作P是与操作F相应的对变量V的read或write操作;类型地,假定动作B是线程T对变量W实施的use或assign动作,假定操作G是操作B相关联的load或store操作,假定操作Q是与操作G相应的对变量V的read或write操作。如果A先于B,那么P先于Q。这条规则要求valitile修改的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同。

4. 对于long和double型变量的特殊规则

Java内存模型要求lock、unlock、read、load、assign、use、store、write这8个操作都具有原子性,但是对于64位的long和double,允许它们的load、store、read和write不用保证原子性。但还是强烈建议虚拟机对long和double实现原子性。

在实际开发中,商用虚拟机几乎都选择把64位数据的读写操作作为原子操作。

5. 原子性、可见性、有序性

原子性:

由Java内存模型直接保证的原子性变量操作包括read、load、assign、use、store和write,一般认为基本数据类型的访问读写是具备原子性的(例外:long和double类型)。

可见性:

可见性是指一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值,以此来实现可见性。

除了volatile外,synchronized和final也能实现可见性。

(1) Synchronized同步块:对一个变量执行unlock之前,必须先把该变量同步回主内存(store、write);

(2) final关键字:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”引用传递出去,在其他线程中就能看见final修饰的值。

有序性:

如果在本线程内观察,所有的操作都是有序的——线程内表现为串行;

如果在一个线程中观察另一个线程,所有操作都是无序的——指令重排序、工作内存和主内存同步延迟。

Java提供了volatile和synchronized两个关键字来保证线程之间操作的有序性:

Volatile:禁止指令重排序;

Synchronized:一个变量在同一时刻只允许一条线程对其进行lock操作,即持有同一个锁的两个同步块只能串行地进入。

6. 先行发生原则

先行发生是指Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,那么在发生操作B之前,操作A产生的影响能被操作B观察到。

Java内存模型中存在的天然的先行发生关系:

(1) 程序次序规则:同一个线程内,按照代码出现的顺序,前面的代码先行于后面的代码,准确的说是控制流顺序,因为要考虑到分支和循环结构。

(2) 管程锁定规则:一个unlock操作先行发生于后面(时间上)对同一个锁的lock操作。

(3) volatile变量规则:对一个volatile变量的写操作先行发生于后面(时间上)对这个变量的读操作。

(4) 线程启动规则:Thread的start( )方法先行发生于这个线程的每一个操作。

(5) 线程终止规则:线程的所有操作都先行于此线程的终止检测。可以通过Thread.join( )方法结束、Thread.isAlive( )的返回值等手段检测线程的终止。

(6) 线程中断规则:对线程interrupt( )方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupt( )方法检测线程是否中断

(7) 对象终结规则:一个对象的初始化完成先行于发生它的finalize()方法的开始。

(8) 传递性:如果操作A先行于操作B,操作B先行于操作C,那么操作A先行于操作C。

总结:一个操作“时间上的先发生”不代表这个操作先行发生;一个操作先行发生也不代表这个操作在时间上是先发生的(重排序的出现)。

时间上的先后顺序对先行发生没有太大的关系,所以衡量并发安全问题的时候不要受到时间顺序的影响,一切以先行发生原则为准。

猜你喜欢

转载自my.oschina.net/u/3342874/blog/1809160