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 缓存,大大影响了性能。如果互相竞争的核心位于不同的插槽,就要额外横跨插槽连接,问题可能更加严重。

微信截图_20210922172847.png

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

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

参考文献

  • Java并发编程的艺术
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值