Java内存模型
Java虚拟机中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各自平台下都能达到一致的内存访问效果。
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量包括实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享。
如下图,Java内存模型中所有的变量都存储在主内存中(可以类比为PC的内存),每个线程还有自己的工作内存(可以类比为PC的处理器与高速缓存)。线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。
Java内存模型定义了以下8中操作来实现主内存和工作内存之间的交互,其中每一个操作都是原子的、不可再分的(对于double和long类型的变量来说,load、store、read和write操作允许有例外)。
- lock:作用于主内存的变量,把一个变量标识为一个线程独占的状态
- unlock:作用于主内存的变量,把一个处于锁定状态的变量释放出来
- read:作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中,以便后面的load动作使用
- load:作用于工作内存的变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中
- use:作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作
- assign:作用于工作内存的变量,把一个从执行引擎收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
- store:作用于工作内存的变量,把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用
- write:作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中
注意,Java内存模型要求read和load操作、store和write操作必须顺序执行,而不是连续执行。
Happens-before(先行发生)原则
如果Java内存模型中所有的有序性都仅仅靠volatile和synchronized来完成,那么有一些操作将变得十分繁琐。但是我们在写Java并发代码时并没有感觉到这一点,这是因为Java语言中有一个“Happens-before”原则。这个原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则我们可以通过几条规则判断并发环境下两个操作之间是否可能存在冲突。
“Happens-before”指的是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、掉用了方法等。下面是Java内存模型中“天然的”先行发生关系,如果两个操作之间的关系不在此列,并且无法通过下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序。
- 程序次序规则:在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变
- 管程锁定规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果(管程是一种通用的同步原语,synchronized就是管程的实现)
- volatile变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见
- 线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见
- 线程终止规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断
- 对象终结规则:就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法
- 传递规则:happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C
Volatile关键字解析
关键字volatile是Java虚拟机提供的最轻量级的同步机制。当一个变量定义为volatile之后,它将具备两种特性:可见性和禁止指令重排序优化。
可见性是指此变量对所有线程的可见性,即当一个线程修改了变量值之后,新值对于其他线程来说是可以立即得知的(普通变量需要回写到主内存中、另一个线程再从主内存读取才行)。但这并不意味着volatile变量的运算一定是线程安全的,因为Java里的运算并非是原子操作。
// 此处应有代码
test(){}
由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍要通过加锁来保证原子性。volatile最常用的一个场景是作为Bool值进行状态判定。
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
- 变量不需要与其他的状态变量共同参与不变约束
Volatile关键字的性能怎么样?volatile变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则会慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。不过即使如此,大多数场景volatile的总开销比锁要低,在volatile和锁之中选择的唯一依据仅仅是volatile的语义能否满足你的需求。