volitale伪共享问题及解决方案
文章目录
- 一、什么是伪共享?
- 二、volatile与伪共享的关系
- 三、伪共享的示例
- 四、如何解决伪共享?
- 1. 缓存行填充(Padding)
- 2. Java 8的@Contended注解
- 五、总结
在并发编程中,
volatile
关键字用于保证变量的可见性和禁止指令重排序,但它无法解决伪共享(False Sharing) 问题,甚至可能因volatile
的内存语义加剧伪共享的性能影响。
一、什么是伪共享?
伪共享是CPU缓存机制导致的性能问题,根源在于CPU缓存行(Cache Line) 的工作方式:
- CPU缓存以「缓存行」为最小存储单位(通常64字节),一个缓存行可以存储多个变量。
- 当多个线程同时操作不同变量,但这些变量恰好位于同一缓存行时,会触发缓存一致性协议(如MESI),导致缓存行频繁失效和刷新,大幅降低性能。
简单说:无关变量共享了同一个缓存行,引发了不必要的缓存竞争,这就是伪共享。
二、volatile与伪共享的关系
volatile
的内存语义会强化伪共享的影响:
volatile
变量的修改会立即刷新到主存,并使其他CPU核心中该变量的缓存副本失效(通过缓存一致性协议)。- 若多个
volatile
变量位于同一缓存行,即使线程操作的是不同变量,也会因其中一个变量的修改导致整个缓存行失效,其他线程不得不重新从主存加载数据,产生额外开销。
三、伪共享的示例
假设两个线程分别修改
VolatileData
中的a
和b
(均为volatile
变量):
public class VolatileData {volatile long a; // 8字节volatile long b; // 8字节
}
由于
a
和b
总大小为16字节(远小于64字节缓存行),它们会被放入同一缓存行。此时:
- 线程1修改
a
→ 缓存行标记为失效 → 线程2读取b
时,发现缓存行失效,必须从主存重新加载。- 线程2修改
b
→ 缓存行再次失效 → 线程1读取a
时又需重新加载。这种频繁的缓存行失效会导致性能下降,这就是
volatile
变量引发的伪共享问题。
四、如何解决伪共享?
核心思路是让每个变量独占一个缓存行,避免多个变量共享缓存行。常见方案有两种:
1. 缓存行填充(Padding)
手动添加无用字段,填充缓存行剩余空间,使目标变量独占64字节缓存行。
示例(针对64字节缓存行,每个long占8字节):
public class VolatileDataWithPadding {volatile long a;// 填充7个long(56字节),加上a的8字节,共64字节,独占一个缓存行long p1, p2, p3, p4, p5, p6, p7;volatile long b;// 同样填充,让b独占另一个缓存行long p8, p9, p10, p11, p12, p13, p14;
}
此时
a
和b
分别位于不同缓存行,线程操作时不会相互影响。
2. Java 8的@Contended注解
Java 8引入
@sun.misc.Contended
注解(需JDK支持),自动为变量添加缓存行填充,无需手动写填充字段。
使用方式:
import sun.misc.Contended;public class VolatileDataWithContended {@Contended // 自动填充,使a独占缓存行volatile long a;@Contended // 自动填充,使b独占缓存行volatile long b;
}
注意:需添加JVM参数启用(默认仅用于JDK内部类):
-XX:-RestrictContended
五、总结
- 伪共享是缓存行机制导致的性能问题,与变量是否为
volatile
无关,但volatile
会加剧其影响。- 解决核心:让并发访问的变量各自独占一个缓存行(通过手动填充或
@Contended
)。- 适用场景:多线程高频读写不同变量的场景(如计数器、并发队列等),普通场景无需处理(会浪费内存)。
通过避免伪共享,可显著提升高并发场景下的性能(实测可能有10倍以上提升)。