本文需要了解:Java内存模型
缓存一致性问题
为了提高CPU从内存中读写的速度,在CPU和和内存中间添加了缓存,有一级缓存,二级缓存,甚至三级缓存,从一级到三级,容量变大,速度变低。对于多个CPU场景,一般是每个CPU都有自己的一级和二级缓存而公用三级缓存。
在多CPU多线程的读写场景下,多个线程有可能会访问同一块内存区域,以写操作为例,多个线程都会把内存中的值读到自己的CPU中,然后修改,然后暂时放到自己的缓存中,这样就导致每个线程保的缓存中对同一变量可能有不同值的现象,即缓存一致性问题。
缓存一致性协议
为了解决缓存一致性问题,通常有两种方法:
- 在总线加LOCK锁
- 缓存一致性协议
在总线加锁是一种阻塞的方式,显然是不高效的。
缓存一致性协议(Cache Coherence Protocol),最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。
MESI的核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
volatile关键字
内存语义
- 可见性
- 有序性
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
volatile是不能保证原子性的,在指令层面,原子性是通过monitorenter
和monitorexit
两个指令实现的,而volatile的指令层面只有lock指令.
在以下两个场景中可以使用volatile
来代替synchronized
:
1、运算结果并不依赖变量的当前值,或者能够确保只有单一的线程会修改变量的值。
2、变量不需要与其他状态变量共同参与不变约束。
比如说,作为线程退出的标记量,或者DCL的单例模式中的使用。
既生synchronized,何生volatile
说到底,synchronized是通过加锁来实现原子性、可见性和有序性的。既然是锁,肯定是有开销的,虽然说自1.6开始又给synchronized添加了轻量级锁,偏向锁,但性能损耗还是有的。而且,加锁其实是一个阻塞的过程。
相比之下,关于二者的性能对比,由于虚拟机对锁实行的许多消除和优化,使得我们很难量化这两者之间的性能差距,但是我们可以确定的一个基本原则是:volatile变量的读操作的性能消耗与普通变量几乎无差别,但是写操作由于需要插入内存屏障所以会慢一些,即便如此,volatile在大多数场景下也比锁的开销要低。