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

千金一刻莫空度,老大无成空自伤。这篇文章主要讲述Java多线程开发|volatile与伪共享问题相关的知识,希望能为你提供帮助。


作者: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与伪共享问题

文章图片

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

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

    推荐阅读