volatile的应用
volatile是轻量级的synchronized,它在多处理器开发中保证共享变量的**“可见性”**,它不会引起线程上下文的切换和调度
若是能够恰当的使用volatile,它的使用和执行成本会比synchronized更低
定义
Java允许线程访问共享变量,为了确保共享变量准确且一致地更新,线程应该通过排它锁单独获得该变量,即volatile
如果一个字段被声明成volatile,Java线程内存确保所有线程看到这个变量的值是一致的
原理
为了提高处理速度,处理器不直接与内存进行通信,而是先将系统内存的数据读到内部缓存(L1、L2或者其他)后再进行操作。
问题在于,操作完成后,处理器并不知道什么时候会写到内存中去。
有volatile变量修饰的共享变量会在进行写操作的时候多出一行以Lock为前缀的汇编代码。
以Lock为前缀的指令在多核处理器下会引发两件事情:
- 将当前处理器缓存行的数据写回到系统内存
- 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效
于是当对声明了volatile的变量进行写操作的时候,JVM就会向处理器发送一条Lock前缀的指令(这就是会多出一条汇编代码的原因)
然而,当写回内存时,假若*“其他处理器的值”*仍然是没有更改的旧值,那么进行计算操作的时候就会出现问题
如何避免这种情况发生呢?
为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议
各处理器通过嗅探在总线上传播的数据来检查自己缓存的数据是否过期,即缓存行对应的内存地址是否被修改,如果已被更改,则会被设置成无效状态,那么对该数据进行修改操作时,就会重新从系统内存里获得数据
volatile的具体实现原则
1. Lock前缀指令会引起处理器缓存回写到内存
Lock前缀指令导致在执行指令期间声言处理器的**LOCK #**信号。
多处理器环境中,该信号确保在声言该信号期间,处理器可以独占任何共享内存。
然而,对于最近的处理器来说,LOCK #信号一般不锁总线,而是锁缓存
在P6和当前处理器中,如果访问的内存区域已经缓存在处理器内部,则不会声言LOCK #信号,反而会锁定这块区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性
该操作被称为**“缓存锁定”**
缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据
2. 一个处理器的缓存回写会导致其他处理器的缓存无效
在多核处理器系统中操作时,IA-32和Intel 64处理器能嗅探其他处理器访问系统内存和它们的内部缓存。
处理器通过嗅探保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致‘
volatile的使用优化
JDK 7的并发包中新增了一个队列集合类LinkedTransferQueue。它在使用volatile变量时,用追加字节的方式来优化队列出队和入队的性能
共计追加了64个字节
对于一些处理器来说,它们的高速缓存行是64个字节宽,不支持部分填充缓存行
如果队列的头结点和尾结点都不足64字节,那么处理器会将它们读到同一个高速缓存行中,每个处理器都会缓存同样的头尾结点
于是当一个处理器试图修改头节点时,会导致整个缓存行被锁定,在缓存一致性机制下,其他处理器不能访问自己缓存中的尾节点,于是就会严重影响出入队的效率
追加了64个字节后,头节点和尾节点不会加载到一个缓存行,在修改时就不会互相锁定
并非所有使用volatile变量的时候都应该追加64字节,在以下两种场景不应该使用该方式:
- 缓存行并非64字节宽的处理器
- 共享变量不会被频繁地写