volatile
volatile
在多线程并发编程中,
- 可见性,对于
volatile 变量的读,线程总是能读到当前最新的volatile 值,也就是任一线程对volatile 变量的写入对其余线程都是立即可见; - 有序性,禁止编译器和处理器为了提高性能而进行指令重排序;
- 基本不保证原子性,由于存在
long/double 非原子性协议,long/double 在32 位x86 的hotspot 虚拟机下允许没有被volatile 修饰的变量读写操作划分为两次进行。但是从JDK9 开始,hotspot 也明确约束所有数据类型访问保持原子性,所以volatile 变量保证原子性可以基本忽略。
在实际的编程中,要注意,除非是在保证仅有一个线程处于写,而其他线程处于读的状态下的时候,才可以使用
volatile 示例
public class VolatileTest {
public static volatile int race = 0;
public static int value = 0;
public static void increase() {
race++;
value++;
}
private static final int THREAD_COUNT = 20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
increase();
}
});
threads[i].start();
}
while (Thread.activeCount()> 1) {
Thread.yield();
}
System.out.println("race: " + race + " value: " + value);
}
}
volatile 实现原理
在
instance = new Singleton(); // instance是volatile变量
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
将带有
- 将当前处理器缓存行的数据写回到系统内存。
- 这个写回内存的操作会使其他处理器里缓存该内存地址的数据失效
该操作主要基于内存模型中的内存屏障来实现;如上图所示,在
LoadLoad 屏障:对于Load1,LoadLoad,Load2 这样的语句,在Load2 及后续读取操作前要保证Load1 要读取的数据读取完毕;LoadStore 屏障:对于Load1,LoadStore,Store2 这样的语句,在Store2 及后续写入操作前要保证Load1 要读取的数据读取完毕;StoreStore 屏障:对于Store1,StoreStore,Store2 这样的语句,在Store2 及后续写入操作前要保证Store1 的写入操作对其他处理器可见;StoreLoad 屏障:对于Store1,StoreLoad,Load2 这样的语句,在Load2 及后续读取操作前,Store1 的写入对所有处理器可见。
- 写屏障与
(StoreStore 、StoreLoad) 屏障的关系:在volatile 变量写之前加入StoreSore 屏障保证了volatile 写之前,写缓冲器中的内容已全部刷回告诉缓存,防止前面的写操作和volatile 写操作之间发生指令重排,在volatile 写之后加入StoreLoad 屏障,保证了后面的读/ 写操作与volatile 写操作发生指令重排,所以写屏障同时具有StoreStore 与StoreLoad 的功能。 - 读屏障与(LoadLoad、LoadStore)屏障的关系:在
volatile 变量读之后加入LoadLoad 屏障保证了后面其他读操作的无效队列中无效消息已经被刷回到了高速缓存,在volatile 变量读操作后加入LoadStore 屏障,保证了后面其他写操作的无效队列中无效消息已经被刷回高速缓存。读屏障同时具有了LoadLoad ,LoadStore 的功能。
缓存行与volatile 优化
/** 队列中的头部节点 */
private transient final PaddedAtomicReference < QNode > head;
/** 队列中的尾部节点 */
private transient final PaddedAtomicReference < QNode > tail;
static final class PaddedAtomicReference < T > extends AtomicReference T > {
// 使用很多4个字节的引用追加到64个字节
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;
// 省略其他代码
}
追加字节能优化性能?这种方式看起来很神奇,但如果深入理解处理器架构就能理解其中的奥秘。让我们先来看看
为什么追加
那么是不是在使用
- 缓存行非
64 字节宽的处理器。如P6 系列和奔腾处理器,它们的L1 和L2 高速缓存行是32 个字节宽。 - 共享变量不会被频繁地写。因为使用追加字节的方式需要处理器读取更多的字节到高速缓冲区,这本身就会带来一定的性能消耗,如果共享变量不被频繁写的话,锁的几率也非常小,就没必要通过追加字节的方式来避免相互锁定。
不过这种追加字节的方式在