[TOC]
Java内存模型(JMM Java Memory Model):屏蔽掉各种硬件和操作系统的内存访问差异,以实现Java程序在各个平台下都能达到一致的内存访问效果。
(在此之前,C/C++直接使用物理硬件和操作系统的内存模型,会由于不同平台上内存模型的差异,导致程序在以套平台上并发完全正常,到另一平台上并发访问经常出错。)
经过长时间验证和修补,JDK1.5(实现了JSR-133)发布后,Java内存模型成熟和完善起来。
JSR-133:Java Memory Model and Thread Specification Revision Java内存模型和线程规范修订。
https://download.oracle.com/otndocs/jcp/memory_model-1.0-pfd-spec-oth-JSpec/
Java内存模型规定了所有的变量都存储在主内存里。
每条线程有自己的工作内存,线程的工作内存保存了被该线程使用到的变量的主内存副本的拷贝。
线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。
不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存完成。
这里所讲的主内存、工作内存与本书第2章所讲的Java内存区域中的Java堆、栈、方法区 等并不是同一个层次的内存划分,这两者基本上是没有关系的
交互协议:一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存
Java内存模型定义了以下8种操作,虚拟机实现时需要保证每个操作都是原子的、不可再分的。
操作的规定
这8中内存操作及上述规定,加上对volatile的特殊规定,完全确定了Java程序中哪些内存操作在并发下是安全的。
最新的JSR-133文档,已经放弃采用这8种操作去定义Java内存模型访问协议了。后面会介绍等效判断原则——先行发生原则,来确定一个访问在并发环境下是否安全。
保证此变量对所有线程的可见性
对于volatile变量,当一个线程修改了这个变量的值,其他线程可以立刻得知新值。
线程在使用变量时,都会去主内存获取一下新的变量值???
禁止指令重排序优化
volatile错误的用法:
1 | /** |
这段代码发起了20个线程,每个线程对race变量进行10000次自增操作,如果这段代码能 够正确并发的话,最后输出的结果应该是200000。但是实际执行的输出结果,每次都是一个小于200000的数字,这说明这段代码并没有做到正确的并发执行。
问题就出现在自增运算“race++”之中。
使用javap查看increase()方法的字节码,可以分析出原因在哪
1 | public static void increase(); |
只有一行代码的increase()方法在Class文件中是由4条字节码指令构成的(return 指令不是由race++产生的,这条指令可以不计算),从字节码层面上很容易就分析出并发失 败的原因了:当getstatic指令把race的值取到操作栈顶时,volatile关键字保证了race的值在此 时是正确的,但是在执行iconst_1、iadd这些指令的时候,其他线程可能已经把race的值加大 了,而在操作栈顶的值就变成了过期的数据,所以putstatic指令执行后就可能把较小的race值 同步回主内存之中。
volatile正确的用法
1 | volatile boolean shutdownRequested; |
***由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通 过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性。 ***
看一个因为指令重排列会导致错误的例子:
1 | Map configOptions; |
下面代码在线程A中执行,解析配置信息,解析配置后,将initialized设为true,通知其他线程
1 | configOptions = processConfigOptions(); |
下面代码在线程B中执行,等待nitialized设为true,对配置信息进行操作。
1 | while (!initialized) { |
如果nitialized变量不用volatile进行修饰,可能会由于重排序优化,导致线程A中initialized=true被提前执行,导致程序错误。
(重排序优化是机器级的优化,在此提前执行值对应的汇编代码被提前执行)
1 | 线程A |
如何禁止执行重排序的?会插入一些内存屏障汇编机器指令,使重排序时无法将指令重排序到内存屏障之前或之后的位置。
volatile变量的注意点
volatile只能保证可见性,在以下场景中,需要使用锁(synchronized 或 juc包中的原子类)来保证原子性。
1)运算结果依赖变量的当前值
2)变量需要与其他的状态变量来共同构成不变性约束
Java内存模型允许没有被volatile修饰的64位数据(long和double)划分为两次32位操作。
目前,商用虚拟机都选择把64位数据的读写作为原子操作看待,因此,写代码时,不需要专门将long和double声明为volatile。
Java内存模型是围绕并发过程中如何处理原子性、可见性和有序性这3个特征来建立的。
哪些操作实现了这3个特征那?
原子性:
1)对于基本数据类型的访问读写是具备原子性的。
2)synchronized块之间操作是原子性的(通过monitorenter和monitorexit两条指令)。
可见性:
可见性指当一个线程修改了共享变量的值,其他线程能够立即得知到。
Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
1)volatile保证可见性:保证新值能够立即同步到主内存,每次使用前从主内存刷新。
Java内存模型是通 过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作 为传递媒介的方式来实现可见性的
2)synchronized同步块保证可见性:对一个变量unlock之前,必须先把此变量同步回主内存
3)final保证可见性:被final修饰的字段在构造器中一但初始化完成,并且构造器没有把”this“引用传递出去,那其他线程就能够看到final字段的值。
有序性:
如果在本线程内观察,所有的操作都是有序的
(线程内表现为串行的语义:方法执行过程中所有依赖赋值结果的地方都能获取到正确的结果,但不保证变量赋值操作的顺序与程序代码中的顺序一致)
如果在一个线程观察另一个线程,所有的操作都是无序的
("指令重排序"现象 和 ”工作内存与主内存同步延迟“现象)。
1)volatile关键字通过禁止指令重排序的语义保证线程之间操作的有序性
2)synchronized通过”一个变量在同一个时刻只运行一个线程对其进行lock操作“这条规则,保证持有同一个锁的两个同步块只能串行进入
先行发生原则指的是什么?
如果操作A先行发生于B,那么在发生操作B之前,操作A产生的影响能够被B观察到,影响包括修改了内存中共享变量的值、调用了方法等。
先行发生原则可以用来做什么那?
可以用于判断数据是否存在竞争、线程是否安全。
Java内存模型下有以下天然的先行发生关系,无须任何同步协助就已经存在。
如果两个操作之间的关系不在此列,且无法根据下列规则推到出来,它们就没有顺序性保障,虚拟机可对他们随意重排序。
线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资 源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以 独立调度(线程是CPU调度的基本单位)。
Java语言则提供了在不同硬件和操作系统平台下对 线程操作的统一处理,每个已经执行start()且还未结束的java.lang.Thread类的实例就代表 了一个线程。我们注意到Thread类与大部分的Java API有显著的差别,它的所有关键方法都 是声明为Native的。在Java API中,一个Native方法往往意味着这个方法没有使用或无法使用 平台无关的手段来实现(当然也可能是为了执行效率而使用Native方法,不过,通常最高效 率的手段也就是平台相关的手段)。