计算机为了提升效率,增加了多内核,并且为每个内核增加了自己的高速缓存,而他们又共享同一个主内存。
除了增加高速缓存外,为了使处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行优化,但保证结果与顺序执行的结果一致。
与处理器的乱序执行优化类似,Java虚拟机的即时编译器也有指令重排序优化。
Java内存模型
《Java虚拟机规范》中曾试图定义一种Java内存模型来屏蔽各种硬件和操作系统的内存访问差异。
主内存与工作内存
Java内存模型的主要目的是定义程序中各种变量的访问规则(关注虚拟机中把变量存储到内存和从内存中取出变量的底层细节。)
Java内存模型规定了所有的变量都存储在主内存(除线程私有的以外)。
线程的工作内存保存了被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存的数据。不同的线程之间也无法直接访问对方工作内存的变量,线程之间变量值的传递均需要通过主内存来完成。
内存间交互操作
关于主内存与工作内存之间的交互协议,Java内存模型中定义了8种的操作。
Java虚拟机实现时必须保证每种操作都是原子的,不可再分的(对于double和long来讲,load、store、、read、write允许有例外)。
lock : 锁定,作用于主内存的变量,把一个变量标识为一条线程独占的状态。
unlock: 解锁,作用于主内存的变量,把一个处于锁定状体的变量释放出来,释放后的变量才可以被其他线程锁定。
read: 读取,作用于主内存的变量,把一个变量的值从主内存传输到工作内存,以便随后的load动作使用。
write: 写入,作用于主内存的变量,把store操作从工作内存中得到的变量值存放到主内存变量中。
load: 载入,作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use:使用,作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
assign: 赋值,作用于工作变量,把一个从执行引擎接收的值赋给工作内存变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store: 存储,把工作内存中一个变量的值传送到内存中,以便随后的write操作使用。
Java内存模型规定必须满足如下规则:
1) 不允许read 和 load、store和write操作单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者工作内存发起回写单主内存不接受
2)不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
3)不允许一个线程在没有发生assign操作就把数据从线程的工作内存同步回主内存中。
4) 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load\assign)的变量,即对一个变量进行use之前必须使用load, 使用store之前必须使用assign。。
5)一个变量在同一个时刻内只允许一个线程对其进行lock操作, 但lock操作可以被同一个线程重复执行多次, 多次执行lock后, 只有执行相同次数的unlock操作,变量才会被解锁。
6)对一个变量执行unlock之前, 必须先把此变量同步回主内存中(执行store、write操作)
volatile型变量的特殊规则
volatile是Java虚拟机最轻量级的同步机制。它有两个特性:
1)保证此变量对于所有线程的可见性(一个线程修改了这个变量的值,其他线程立即可见,也就是每个线程在使用被volatile修饰的变量时,都会强制从主内存中同步该变量的值到工作内存中)
在不符合以下两条规则的场景中,仍然需要通过加锁来保证原子性(synchronized、java.util.concurrent中的锁或者原子类):
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值;
- 变量不需要与其他状态变量共同参与不变约束;
2) 禁止指令重排序优化
volatile变量读操作的性能与普通变量几乎没有差别,但是写操作会慢些, 因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
Singleton DoubleCheck
1 | public class Singleton { |
在上面的代码中,instance = new Singleton()最终会被编译成多条汇编指令。
(1)为Singleton的实例分配内存。
(2)调用Singleton的构造函数,初始化成员变量
(3)将instance对象指向分配的内存空间。
或者可能被重排序为(1)、(3)、(2), 也就是当分配内存后,如果没有对instance 加volatile,将instance对象指向分配的内存空间后,另外一个线程可以读到未被初始化的对象。
针对long和double类型变量的特殊规则
Java内存模型要求lock、unlock、read、write、 load、assign、use、store这8种操作都具有原子性,但是对于64位数据类型的long 和double, 在模型中定义了一条宽松的规定, 允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为2次32为的操作来执行,也就是虚拟机自己选择是否要保证64位数据类型的load、store、read、write这四个操作的原子性。
经过实际的测试,在目前的主流平台下商用的64位Java虚拟机中并不会出现非原子性访问行为,但是对于32位的Java虚拟机(如32位的X86Hotspot)存在非原子性访问的风险。
因此,在实际开发中,除非该数据有明确可知的线程竞争,否则我们在编写代码的时候一般不需要因为这个原因刻意把用到的long和double变量专门声明位volatile。
原子性、可见性与有序性
原子性(Atomicity)
有Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store、write。因此基本数据类型的访问都是具备原子性的。
如果应用场景需要一个更大范围的保证,需要使用锁、或者synchronized关键字来保证原子性。
(lock、unlock并没有直接开放给用户使用)。
可见性(visibility)
可见性就是当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。
volatile
volatile的特殊规则保证了新值立即能同步到主内存,以及每次使用前都立即从主内存刷新。而普通变量则不能保证“立即”。
synchronized
同步块的可见性时由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中”这条规则获得。
final
被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this的引用传递出去,那么在其他线程中就能看见final字段的值。
this引用逃逸,其他线程有可能通过这个引用访问到“初始化了一般”的对象。
有序性
如果在本线程中观察,所有的操作都是有序的(线程内似表现为串行的语义);
如果在一个线程中观察另一个线程,所有的操作都是无序的(指令重排序和工作内存与主内存同步延迟现象);
Java语言提供了volatile和sychronized两个关键字来保证线程之间的有序性。
volatile:本身包含了禁用指令重排序的语义;
synchronized:一个变量在同一时刻只允许一个线程对其进行lock操作;
先行发生原则
先行发生原则:判断数据是否存在竞争,线程是否安全的重要手段。
程序次序规则:在一个线程内,按照控制流顺序(分支、循环等),书写在前面的操作先行发生在书写在后面的操作;
管程锁定规则:一个unlock的操作先行发生在后面对同一个锁的操作。
volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作;
线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作;
线程的中止规则:线程中所有的操作都先行发生于此线程的终止检测,可以通过thread.join()方法是否结束、thread.isAlive()返回值来检测线程是否已经停止;
线程的终端规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过thread.interrupted()检测是否有中断发生;
对象终结规则:一个对象初始化完成(构造函数执行结束)先行发生于他的finalize()方法的开始;
传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出,操作A先行发生于操作C。