Java内存模型(JMM)
原子性(Atomicity)
原子性是指一个操作是不可中断的。即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
比如,一个静态全局变量 int i,两个线程同时对它赋值,线程A给他赋值1,线程B给它赋值-1。那么不管两个线程怎么工作,i的值只能是1或则-1.线程A和线程B之间没有干扰。
对于 32
位系统的来说, long
类型数据和 double
类型数据(对于基本数据类型, byte
, short
, int
, float
, boolean
, char
读写是原子操作),它们的读写并非原子性的
指令重排
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排。指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。
编译器优化的重排
编译器重排
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
指令并行的重排
处理器重排
现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序
内存系统的重排
处理器重排
由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。
编译器重排
有两个线程如下:
Thread 1 Thread2
1:r2=A; 3:r1=B;
2:B=1; 4:A=2;
上述两个线程同时执行,分别有1、2、3、4四段执行代码,其中1、2属于线程1 , 3、4属于线程2 ,从程序的执行顺序上看,似乎不太可能出现r1 = 1 和r2 = 2 的情况,但实际上这种情况是有可能发现的,因为如果编译器对这段程序代码执行重排优化后,可能出现下列情况:
Thread 1 Thread2
2:B=1; 4:A=2;
1:r2=A; 3:r1=B;
这种执行顺序下就有可能出现 r1 = 1
和 r2 = 2
的情况,这也就说明在 多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的
。
处理器重排
处理器指令重排是对CPU的性能优化,从指令的执行角度来说一条指令可以分为多个步骤完成,如下:
- 取指 IF
- 译码和取寄存器操作数 ID
- 执行或者有效地址计算 EX
- 存储器访问 MEM
- 写回 WB
可见性(Visibility)
可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。
对于串行程序来说,可见性问题是不存在的。因为你在任何一个操作中修改了某个变量,下个操作读取这个变量的值,一定是修改后的新值。
但在多线程环境中可就不一定了,由于线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题。
指令重排以及编译器优化也可能导致可见性问题。
有序性(Ordering)
有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的。
对于多线程环境,则可能出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致,要明白的是,在Java程序中,倘若在本线程内,所有操作都视为有序行为,如果是多线程环境下,一个线程中观察另外一个线程,所有操作都是无序的
,前半句指的是单线程内保证串行语义执行的一致性,后半句则指指令重排现象和工作内存与主内存同步延迟现象。
JMM提供的解决方案
原子性问题
除了
JVM
自身提供的对基本数据类型读写操作的原子性外,对于方法级别或者代码块级别的原子性操作,可以使用synchronized
关键字或者重入锁(ReentrantLock
)` 保证程序执行的原子性工作内存与主内存同步延迟现象导致的可见性问题
可以使用
synchronized
关键字或者volatile
关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。对于指令重排导致的可见性问题和有序性问题
可以利用
volatile
关键字解决,因为volatile
的另外一个作用就是禁止重排序优化。happens-before
原则
JMM中的 happens-before 原则
虽然JVM和执行系统会对指令进行一定的重排,但是指令重排是有原则的(减少程序开发的复杂性),并非所有的指令都可以随便改变执行位置。
程序顺序原则
即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
锁规则
解锁(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()方法