商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > 并发编程中的伪共享(False Sharing):分析 @Contended 注解如何通过内存对齐隔离变量缓存行

并发编程中的伪共享(False Sharing):分析 @Contended 注解如何通过内存对齐隔离变量缓存行

  发布于2026-07-08 阅读(0)

扫一扫,手机访问

先说几个核心判断:伪共享这玩意儿,还真不是你代码写错了,而是多线程在底层被硬件“坑”了一把。当多个 CPU 核心上的线程,各自修改同一缓存行里不同变量时,MESI 协议就得反复做一致性协调——你改一下,我失效一次;我再改一下,你又失效一次。一来二去,性能直接崩掉。而 @Contended 注解干的事很简单:告诉 JVM 在字段前后塞一堆填充字节,把它跟旁边字段彻底隔开,独占整个缓存行。

并发编程中的伪共享(False Sharing):分析 @Contended 注解如何通过内存对齐隔离变量缓存行

伪共享为什么发生:缓存行 + MESI 是关键

CPU 读写内存,不是按字节来的,而是以缓存行为单位——主流是 64 字节。当两个线程分别更新同一缓存行上的 valueAvalueB 时,好戏就上演了:

  • 线程1 改 valueA → Core1 缓存行标记为 Modified
  • MESI 协议一看,不行,得让 Core2 里那行失效,于是标记为 Invalid
  • 线程2 想改 valueB → 发现自己的缓存行无效了,只能重新从内存加载整个 64 字节
  • 接着线程2 修改 valueB → 反过来又把 Core1 那行打失效……

这么一来一回,就成了高频“拉锯战”。结果很统一:吞吐量掉到原来的 1/5 到 1/2,CPU 跑满、但 jstack 抓不到锁、日志里也看不到任何异常。这才是最让人头疼的地方——没有直接错误,只有性能烂。

@Contended 怎么实现内存隔离

它的思路很简单:不动逻辑,只动手脚。在被标记字段的前后各插一段填充(padding),把字段“包”成一个孤岛,保证它前后至少空出一个缓存行的距离。

  • 默认填充宽度是 128 字节——覆盖 64 字节缓存行,还留了安全余量
  • -XX:ContendedPaddingWidth=64 可以显式设为标准缓存行大小
  • 支持分组:@Contended("counter") 能让同组字段打包后再整体填充。比如 head 和 tail 这两个本就需要共处一组的变量,可以放在一起隔离,但又不会和外部字段互相干扰

这就有意思了——你不只可以隔离单个字段,还能按业务逻辑把热点字段分到同一组里,内部共享、外部隔离。

必须配齐的 JVM 启动参数

需要特别警惕的是,@Contended 是“有条件启用”的特性,缺一个参数它就不干活:

  • -XX:+UnlockExperimentalVMOptions(JDK 8/9 必须开启,否则直接忽略)
  • -XX:+UseContended(JDK 8) 或 -XX:-RestrictContended(JDK 9+)

另外还有几个隐性规则:静态字段加了也白加;public 字段会被 JVM 直接无视;必须是实例字段,且访问权限为 private/protected 或包级。这几个坑踩的人不少,记得检查到位。

不用 @Contended 的替代方案

如果环境不支持注解,或者你更想掌控一切,手动填充也是个靠谱的选择,兼容性反而更好:

  • 在热点字段前后各塞 7 个 long(7×8=56 字节),加上字段本身凑够 64 字节隔离带
    写法就是 long p1, p2, p3, p4, p5, p6, p7;
  • 借助 JOL(Ja va Object Layout)来验证布局:
    执行 ClassLayout.parseClass(YourClass.class).toPrintable(),可以直观看到字段真实偏移
  • 还有一个容易忽略的点:ja vac 不保证字段声明顺序等于内存顺序,最终布局由 JVM 对齐策略决定。所以就算你代码里把缓存行隔离字段写在前面,实际运行时可能并不按你想象的位置排。用 JOL 看一遍,一切就清楚了。

说到底,理解伪共享不是为了炫技,而是让你在排查性能问题时多一个方向——下次遇到 CPU 高、锁不明显的场景,不妨先看看缓存行上是不是正打着一场“看不见的战争”。

本文转载于:https://www.php.cn/faq/2417832.html 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注