JVM内存模型
什么是Java内存模型
Java内存模型即Java Memory Model
,简称JMM
。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。
想要理解Java的并发机制,就必须先了解JMM,JMM的关键技术点都是围绕多线程的原子性、可见性和有序性建立起来的。
线程之间的通信
想要控制线程之间的状态和先后顺序,线程之间需要通信,当自己占有临界资源时,告知别的线程挂起;当自己离开临界区时,唤醒别的线程。
- 共享内存:线程之间共享程序的公共状态,线程之间通过读写公共状态来隐式通信,比如通过共享对象通信;
- 消息传递:线程之间没有公共状态,线程之间必须通过明确的消息发送来进行显示通信,比如Java里的
wait()
和notify()
,线程之间的通信(thread signal。
线程之间的同步
同步是指在需要竞争的情况下,程序用于控制不同线程之间操作发生相对顺序的机制。
- 在共享内存并发模型中,同步是显式的。程序必须指定在何处需要线程互斥,比如Java中的同步方法、同步代码块等
- 在消息传递的并发模型中,消息的发送和接收本来就有先后顺序,因此同步是隐式的。
JAVA的内存模型
Java的并发采用的是共享内存模型,JMM决定一个线程对共享变量的写入何时对另一个线程可见。线程本地内存和主内存之间的抽象关系为:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了主内存中共享变量的副本,线程中的读写操作都是对副本的操作,JMM决定何时将副本替换到主内存中。
这种模型是一个抽象概念,并不是真实存在的,全面理解Java内存模型(JMM)及volatile关键字
原子性
原子操作是不可中断的,也就是说是互斥的,即使是多个线程一起执行同一个原子操作,一旦某个线程已经开始,就不会受到别的线程的干扰。
指令重排
- 编译器优化的重排:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。比如对不同变量的赋值操作,虽然在单线程中对不同变量赋值的结果是顺序无关的,编译器可以任意指定顺序,但是在多线程中,多个线程对同一个共享变量赋值,就会导致变量的结果出现无法预测的情况;
- 指令并行的重排:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序。CPU指令是按流水线技术来执行的,但是一旦因为某些原因出现流水终端,比如操作数还没准备好,那么所有硬件设备就会停止一个或者多个周期,会造成CPU性能下降,指令重排可以优化这种中断,可以在中断时把后面与这个指令无关的指令先执行,等到下一个周期再执行中断的指令,如果中断的指令还没有准备好,那继续执行后面的指令,直到中断的指令可以执行。单线程的情况下没有问题,因为可以确定各个指令在线程内是否相关,但是对于多线程的情况,无法确定指令间是否相关,因此可能导致错误的执行顺序;
- 内存系统的重排:由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。
可见性
可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。单线程没有可见性的说法,但是多线程因为上面的指令重排、Java的共享变量操作机制等,可能会导致执行混乱,一个线程取到的主内存共享变量值可能并不是最新的值,各个线程中的本地内存副本对别的线程并不可见,因为存在本地内存和主内存同步的延迟,可见性也是一个问题
有序性
和可见性类似,单线程的代码执行都是按照程序执行顺序来的,但因为指令重排、主内存和本地内存的同步延迟,导致多线程之间的执行顺序看起来是无序的。
JMM的解决方案
- JVM自身提供了一套对基本数据类型读写操作的原子性;
synchronized
或ReentrantLock
保证方法或代码块级别的原子性;volatile
和synchronized
可以保证共享变量的可见性,synchronized
是通过原子性保证读写操作之间没有延迟,但volatile
修饰的共享变量是保证该 共享变量对所有线程是可见的,当一个线程要读取共享变量时,JMM会强行让线程内本地内存中的副本无效,直接到主内存中读取;当一个线程要写共享变量时,JMM会把对应的本地内存副本值刷入主内存当中。此外volatile
变量还可以禁止指令重排,但不能保证原子性。happens-before
原则保证多线程环境下两个操作间的原子性、可见性以及有序性,如果处处都需要通过显示地指定,那开发将非常麻烦,JMM有一个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()方法
内存屏障
真正实现禁止指令重排的,是内存屏障,它的作用是在两个指令间插入一条Memory Barrier,告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier重排,也就保证了顺序执行。