bigywater.tech
Java · 287

@Contended 与伪共享

两个线程各改各的变量,却因为落在同一条缓存行里互相拖慢。这篇量出这个代价,再对比手工填充与 @Contended,并读出 JVM 实际排的字段偏移。

2981 → 447 ms
相邻 volatile long 与填充后各自自增 1 亿次的中位耗时
128 字节
开启 @Contended 后字段前后的填充宽度(默认)
静默忽略
不加 -XX:-RestrictContended 时用户类上的 @Contended

字段偏移

实测 · 本机执行输出
  默认(不加 -XX:-RestrictContended):
    Adjacent [a@16, b@24] 最大偏移+8=32
    Padded [a@144, b@16] 最大偏移+8=152
    Cont [a@16, b@24] 最大偏移+8=32
    Group [a@16, b@24, c@32] 最大偏移+8=40
    Hot [a@16, b@24] 最大偏移+8=32
  加 -XX:-RestrictContended:
    Adjacent [a@16, b@24] 最大偏移+8=32
    Padded [a@144, b@16] 最大偏移+8=152
    Cont [a@144, b@280] 最大偏移+8=288
    Group [a@144, b@152, c@288] 最大偏移+8=296
    Hot [a@152, b@16] 最大偏移+8=160
  加 -XX:-RestrictContended -XX:ContendedPaddingWidth=64:
    Adjacent [a@16, b@24] 最大偏移+8=32
    Padded [a@144, b@16] 最大偏移+8=152
    Cont [a@80, b@152] 最大偏移+8=160
    Group [a@80, b@88, c@160] 最大偏移+8=168
    Hot [a@88, b@16] 最大偏移+8=96

耗时对比

实测 · 本机执行输出
  相邻 a、b                                     [2996, 2851, 2981]  中位 2981 ms
  手工填充 15 个 long                             [442, 447, 451]  中位 447 ms
  @Contended 但没开 -XX:-RestrictContended      [2785, 2762, 2853]  中位 2785 ms
  @Contended + -XX:-RestrictContended        [447, 444, 452]  中位 447 ms

分步演示

伪共享与 @Contended
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12,Apple Silicon(arm64);JOL 在本环境自附加时进程被挂起,改用 sun.misc.Unsafe 读偏移
    • 没有验证:x86 上的缓存行大小与结果(本机上的 128 字节填充是否等于真实缓存行大小没有量)、多于两个线程的扩展性、LongAdder 的 Cell 内部填充、非 volatile 字段的伪共享差异、JDK 27 的布局
    京ICP备2026067502号-1京公网安备11010502063487号