bigywater.tech
Java · 232

AQS 自定义同步器:从状态位到等待队列

ReentrantLock、Semaphore、CountDownLatch 背后都是 AQS。继承它只需要实现几个 try 方法,排队、阻塞、唤醒由框架负责。这篇亲手写四个同步器并运行,验证独占与共享模式、公平性和 Condition 的行为。

3
3 个许可的信号量下,同时持有许可的线程数最大值
1×
非公平 tryAcquire 对公平 tryAcquire 的总次数之比(8 线程 1.5 s)
0
await 之后锁的 state(锁被完全释放)

独占模式:不可重入互斥锁

实测 · 本机执行输出
  4 个线程各加锁 50000 次,计数 = 200000(期望 200000);结束时 state = 0,hasQueuedThreads = false
  持有锁时:state = 1,isHeldExclusively = true,AQS.toString() = …Sync@…[State = 1, empty queue]
  同一线程再次 lock(不可重入):tryAcquireNanos(300 ms) → false(耗时 303 ms);这是一把不可重入锁,同线程第二次 lock() 会死锁
  没有持有时 unlock → IllegalMonitorStateException: 不是持有者(这是 tryRelease 里自己抛的)
  另一个线程 acquireInterruptibly 阻塞中:getQueueLength = 1,队列里的线程状态 = WAITING
  中断它 → InterruptedException;之后 getQueueLength = 0(取消的节点被清理)

共享模式:信号量与闩锁

实测 · 本机执行输出
  3 个许可、12 个线程各 20 次:同时持有许可的线程数最大 = 3(应 ≤ 3),结束时 state = 3
  共享模式闩锁 Gate(3),5 个线程在 await:getQueueLength = 5,已通过 0
  countDown 2 次后:state = 1,已通过 0
  第 3 次 countDown(state 变 0)之后,5 个等待者全部通过,——一次 releaseShared 唤醒了队列里所有的共享等待者(传播),已通过 5
  Sem(0),4 个线程等待:release 1 个许可后通过 1 个;再 releaseShared(3) 后共通过 4 个

等待队列与公平性

实测 · 本机执行输出
  主线程持有锁,W0..W3 依次(间隔 30 ms)来排队:getQueuedThreads() 返回的顺序 = [W3, W2, W1, W0](文档不保证顺序,这里是最近排队的在前);hasQueuedPredecessors() = true(对一个新来的线程来说前面有人)
  非公平 tryAcquire(直接 CAS):8 个线程循环加锁 1.5 s,总次数 155738178,最多的线程 29733070 次,最少的线程 14646912 次(最多/最少 = 2.0 倍)
  公平 tryAcquire(先 hasQueuedPredecessors):8 个线程循环加锁 1.5 s,总次数 642869,最多的线程 80440 次,最少的线程 80334 次(最多/最少 = 1.0 倍)

Condition

实测 · 本机执行输出
  用 AQS 的 ConditionObject 写容量 4 的有界缓冲区,生产 20000 个、消费求和 = 200010000(期望 200010000)
  没有持有锁就 signal → IllegalMonitorStateException(来自 isHeldExclusively 返回 false)
  另一个线程拿到锁并 await 之后:state = 0(await 把锁完全释放了,所以主线程现在能拿到锁)
  主线程持有锁时:hasWaiters = true,getWaitQueueLength = 1,锁的等待队列长度 getQueueLength = 0
  signal 之后、主线程释放锁之前:被唤醒的线程被移到锁的等待队列:getQueueLength = 1,getWaitQueueLength = 0
  主线程释放锁之后,被唤醒的线程拿到锁并结束:存活 = false

分步演示

AQS 自定义同步器
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12,10 核 macOS
    • 没有验证:AQS 内部的节点队列结构与 CLH 变体(这篇只用公开的方法观察)、可重入锁的完整写法、tryAcquireSharedNanos 的超时行为、大量线程竞争下的吞吐曲线;公平与非公平吞吐的对比用了很短的临界区,不代表一般情况
    京ICP备2026067502号-1京公网安备11010502063487号