Java 内存模型
文章目录
前言
今天来学一下 Java
内存模型。
CPU 工作模型
在学 Java
内存模型之前,我们来了解一下 CPU
的工作模型。
现代计算机中的 CPU
的执行速度远高于内存的读取速度, CPU
每读取一次都要都要等待老半天,这样 CPU
就会有大量的时间在等待,而浪费了 CPU
的计算能力。
为了尽可能的充分利用 CPU
,于是在 CPU
和内存之间加了一个高速缓存(Cache)作为缓冲,这时 CPU
就从高速缓存中读取数据,不用每次都从内存中读取,大大提高了 CPU
利用率, CPU
在运算完后会把高速缓冲数据再刷回内存。
这么做虽然提高了 CPU
的利用率,但是也带来了新的问题,也就是缓存一致性。
缓存一致性
先来看如下程序
|
|
程序执行的时候, t1
和 t2
线程会把变量 i
读取到各自的高速缓存中,之后对 i
的计算都只会对高速缓存中进行运算。
所以在 t1
和 t2
线程中读取到的可能都是 0
,这时 t1
对 i
加1, t2
缓存中的 i
还是 0
也进行加1。当它们执行完之后会把计算的结果刷回主内存,这时候 i
的值应该是 2
的,结果却由于缓存的原因导致结果不是预期的。这就是多个 CPU
操作同一块内存时,每个 CPU
都有自己缓存,导致的缓存一致性问题。
指令重排
编译器为了让 CPU
能够更好的计算,在编译的时候会对代码进行优化。比如:进行指令重排。有如下代码:
|
|
经过指令重排后可能会变成如下样子
|
|
正常情况下指令重排并不会有太大的影响,但是在多线程下的结果可能大大出乎意料。
有了上面的问题 Java
虚拟机规范就提出了 Java
内存模型来解决这个问题。
Java 内存模型
Java
内存模型是用来屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java
程序在各种平台下都能达到一致的内存访问效果。
在 Java
内存模型中使用工作内存来类比 CPU
的高速缓存,每个线程都有一个工作内存,工作内存中保存了共享变量的副本。
Java
内存模型描述的是多线程并发、 CPU
缓存相关的内容,和 Java
内存结构不是同一个东西。
在内存模型中有一个 happens-before
原则,来了解一下。
happens-before原则
这个原则非常重要,它是判断数据是否存在竞争,线程是否安全的非常有用的手段。依赖这个原则,我们可以通过几条简单规则一揽子解决并发环境下两个操作之间是否可能存在冲突的所有问题,而不需要陷入 Java
内存模型苦涩难懂的定义之中。
程序次序规则(Program Order Rule)
在同一个线程中写在前面的代码对写在后面的代码可见。这里要注意如果有循环分支的情况,它们的顺序跟控制流的顺序有关。
也就是在单线程中,先执行代码的结果对后面的操作是可见的。
管程锁定规则(Monitor Lock Rule)
一个 unlock
操作先行发生于后面对同一个锁的 lock
操作。这里必须强调的是“同一个锁”,而“后面”是指时间上的先后。
volatile变量规则(Volatile Variable Rule)
对一个 volatile
变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的。也就是对一个被 volatile
修饰的变量进行写操作,对后面的读操作一定是可见的。在多线程的情况下,可以保证数据的正确性。
线程启动规则(Thread Start Rule)
Thread
对象的 start()
方法先行发生于此线程的每一个动作。很好理解只有线程启动了,才会去执行线程里的逻辑。
线程终止规则(Thread Termination Rule)
线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过 Thread::join()
方法是否结束、 Thread::isAlive()
的返回值等手段检测线程是否已经终止执行。
线程中断规则(Thread Interruption Rule)
对线程 interrupt()
方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread::interrupted()
方法检测到是否有中断发生。
对象终结规则(Finalizer Rule)
一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize()
方法的开始。
传递性(Transitivity)
如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
总结
Java
内存模型的出现是由于CPU
缓存和指令重排导致在多线程情况下结果不符合预期。Java
内存模型是一套规范,最重要的是happens-before
原则。
参考
- Java内存模型与线程
- 《深入理解Java虚拟机》