bigywater.tech
Java · 153

StampedLock:乐观读到底是不是真正无锁

「乐观读」到底是不是无锁?这篇用小实验把 stamp、validate、读到不一致数据的可能性和吞吐一个个量出来(ReentrantLock 的排队见前面 AQS 一篇)。

stamp / validate
乐观读只拿版本号,写过之后旧版本号失效
2417 次
验证之前读到 x+y≠100 的次数(validate 通过后是 0)
2756 vs 2.4
乐观读与 StampedLock 读锁的吞吐(百万次/秒)

stamp 与 validate

实测 · 本机执行输出
  tryOptimisticRead() 得到 stamp = 256,没有写者时 validate = true
  写锁被持有(stamp = 384)期间,旧 stamp 的 validate = false
  写锁释放之后,旧 stamp 的 validate = false(写过一次,乐观读的版本永远失效);新的 tryOptimisticRead() = 512
  写锁被持有时 tryOptimisticRead() 返回 0(0 表示失败)
  只有读锁被持有时 tryOptimisticRead() 返回 非 0(成功),不加任何锁、也不阻塞写者:tryWriteLock 在读锁被持有时 失败
  不可重入:持有写锁时再 tryWriteLock() = 0(0 = 失败,不会像 ReentrantLock 那样重入);同一线程再 writeLock() 会永远阻塞(这里没有执行)

为什么要验证

实测 · 本机执行输出
  写线程持续修改 (x, y)(不变式 x + y == 100);读线程做了 34581706 次乐观读:
    验证之前读到 x + y != 100 的次数 = 2417(读到了写到一半的数据)
    validate() 返回 false 的次数 = 34484958;validate 通过却读到不一致的次数 = 0

读吞吐

实测 · 本机执行输出
  synchronized             8 个读线程 + 每 10 ms 一次写:读吞吐   40.3 百万次/秒
  ReentrantReadWriteLock   8 个读线程 + 每 10 ms 一次写:读吞吐    1.9 百万次/秒
  StampedLock 悲观读锁         8 个读线程 + 每 10 ms 一次写:读吞吐    2.4 百万次/秒
  StampedLock 乐观读          8 个读线程 + 每 10 ms 一次写:读吞吐 2756.0 百万次/秒

分步演示

StampedLock
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12,机器 10 核
    • 没有验证:写者饥饿与读者饥饿的公平性、锁升降级(tryConvertToWriteLock)、asReadLock 等视图、与 volatile/VarHandle 的对比、JDK 17 与 27 的数字;吞吐只有一次 1.5 秒的运行,synchronized 的数字波动很大
    京ICP备2026067502号-1京公网安备11010502063487号