作者:Sparkle 来源:​​恒生LIGHT云社区​

volatile是轻量级的synchronized,他在多线程开发中保证了共享变量的“可见性”。

可见性:当一个线程修改一个共享变量时,另外一个线程能够读到这个修改的值。

实际上我们需要记住volatile的三层语义:

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

1. volatile的定义与实现原理

定义:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了 ​​volatile​​关键字,在某些情况下比锁要更加方便。如果一个字段被声明成 ​​volatile​​,Java线程内存模型确保所有线程看到这个变量的值是一致的。

那么volatile是如何保证可见性的呢?

有 ​​volatile​​变量修饰的共享变量进行写操作的时候会多出一行汇编代码

lock addl $0×0,(%esp);

​Lock#​​前缀的指令在多核处理器下引发了两件事

  1. 将当前处理器缓存行的数据写回到系统内存。
  2. 这个写回内存的操作会使在其他 CPU里缓存了该内存地址的数据无效。处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。

总线(也叫CPU总线)是所有CPU与芯片组连接的主干道,负责CPU与外界所有部件的通信,包括高速缓存、内存、北桥,其控制总线向各个部件发送控制信号、通过地址总线发送地址信号指定其要访问的部件、通过数据总线双向传输。

2. volatile的使用优化

著名的Java并发编程大师 ​​Doug lea​​在 ​​JDK 7​​的并发包里新增一个队列集合类 ​​LinkedTransferQueue​​,它在使用 ​​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;
// 省略其他代码
}

它使用一个内部类类型来定义队列的 头节点(head)和尾节点(tail),而这个内部类 ​​PaddedAtomicReference​​相对于父类 ​​ AtomicReference​​只做了一件事情,就是将共享变量追加到64字节。

那么看到这里,小伙伴们大多会有疑惑:为什么追加字节能够提高并发编程的效率? 那么就需要先了解一下缓存行的概念。

缓存行:缓存中可以分配最小存储单位。缓存行大小一般为32~256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,若这些变量共享一个缓存行的话,就会影响彼此的性能,即伪共享问题

3. 伪共享问题

举例说明:在核心1上运行的线程想更新变量 ​​X​​,同时核心2上的线程想要更新变量 ​​Y​​。不幸的是,这两个变量在同一个缓存行中。每个线程都要去竞争缓存行的所有权来更新变量。如果核心1获得了所有权,缓存子系统将会使核心2中对应的缓存行失效。当核心2获得了所有权然后执行更新操作,核心1就要使自己对应的缓存行失效。这会来来回回的经过 ​​L3​​缓存,大大影响了性能。如果互相竞争的核心位于不同的插槽,就要额外横跨插槽连接,问题可能更加严重。

Java多线程开发|volatile与伪共享问题_volatile

为什么追加64字节能够提高并发编程的效率?

因为对于绝大多数处理器的L1、L2或L3缓存的高速缓存行是64个字节宽,且不支持部分填充缓存行。这意味着,如果队列的头节点和尾节点都不足64字节的话,处理器会将它们都读到同一个高速缓存行中,在多处理器下每个处理器都会缓存同样的头、尾节点,当一个处理器试图修改头节点时,会将整个缓存行锁定,那么在缓存一致性机制的作用下,会导致其他处理器不能访问自己高速缓存中的尾节点,而队列的入队和出队操作则需要不停修改头节点和尾节点,所以在多处理器的情况下将会严重影响到队列的入队和出队效率。​​Doug lea​​使用追加到64字节的方式来填满高速缓冲区的缓存行,避免头节点和尾节点加载到同一个缓存行,使头、尾节点在修改时不会互相锁定。

参考文献

  • Java并发编程的艺术