volatile的定义:Java编程语言允许线程访问共享变量,为了保证共享变量能被准确和一致性的更新,线程应该确保通过排它锁单独获得这个变量。
和volatile相关的术语:
术语 | 英语单词 | 术语描述 |
内存屏障 | memory barriers | 是一组处理器指令,用于实现对内存操作的顺序限制 |
缓冲行 | cache line |
CPU高速缓存中可以分配的最小存储单位。 处理器填写缓存行时会加载整个缓存行,现在CPU需要执行几百次CPU指令 |
原子操作 | atomic operations | 不可终端的一个或一系列操作 |
缓存行填充 | cache line fill |
当处理器识别到从内存中读取操作数是可缓存的, 处理器读取整个高速缓存行到适当的缓存(L1,L2,L3或其他) |
缓存命中 | cache hit |
如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器从缓存中读取操作数,而不是从内存读取 |
写命中 | write hit |
当处理器将操作数写回到一个内存缓存的区域时,它首先会检查这个缓存的内存地址是否在缓存行中 如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是写回内存,这个操作被称为写命中 |
写缺失 | write misses the cache | 一个有效的缓存行被写入到不存在的内存区域 |
那么,volatile是如何保证可见性呢?
instance = new Singleton();//instance是volatile
转变成汇编代码为
0x01a3de1d: movb $0x0,0x1104800(%esi); 0x01a3de24: lock addl $0x0,(%esp);
有volatile变量修饰的共享变量进行写操作时会多出lock指令,而lock前缀的指令会引发两件事情
- 将当前处理器缓存行的数据写回到系统内存
- 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据写入到内部缓存(L1,L2等)后再进行操作,但是操作完全不知道何时会写入内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到内存中。但是就算写回到内存中,其他处理器缓存的值还是旧的,再执行计算操作就有问题。所以在多处理器下,会实现缓存一致性协议。每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行操作时,会重新从系统的内存中将数据读到处理器缓存中。
volatile的两条实现原则
- Lock前缀指令会引起处理器缓存回写到内存。(Lock信号确保在声明该信号期间,处理器可以独占任何共享内存。但是Lock一般不锁总线,而是锁缓存,毕竟锁总线的开销大)
- 一个处理器的缓存回写到内存会导致其他处理器的缓存无效(处理器通过嗅探技术保证它的内部缓存,系统内存和其他处理器上的缓存数据在总线上保持一致,如果通过嗅探一个处理器来监测其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理器将它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充)
volatile的优化
在JDK1.7的并发包中新增一个队列集合类LinkedTransferQueue
/** head of the queue */ private transient final PaddedAtomicReference < QNode > head; /** tail of the queue */ private transient final PaddedAtomicReference < QNode > tail; static final class PaddedAtomicReference < T > extends AtomicReference < T > { // enough padding for 64bytes with 4byte refs Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe; PaddedAtomicReference(T r) { super(r); } } public class AtomicReference < V > implements java.io.Serializable { private volatile V value; //省略其他代码 }
LinkedTransferQueue定义了一个头节点和一个尾节点,内部类PaddedAtomicReference只做了一件事,将共享变量追加到64字节。
那么为什么追加到64字节会提高效率,因为对于一些主流CPU的L1,L2L3的高速缓存是64字节,通过追加64字节方式填满高速缓存去的缓存行,避免头节点和尾节点加载同一个缓存行,使头、尾节点在修改时不会相互锁定。
那么是不是使用volatile变量时,都应该追加64字节?NO
- 缓存行非64字节宽的处理器
- 共享变量不会被频繁地写
摘自《java并发编程的艺术》
来源:https://www.cnblogs.com/gudulijia/p/6759295.html