公平模式下线程严格按acquire()调用时序排队,由hasQueuedPredecessors()前置检查决定是否入队;返回true则必须入队,false才CAS竞争;该机制保障FIFO但不提升吞吐,仅增强延迟可预测性。

公平模式下线程排队逻辑由 hasQueuedPredecessors() 决定
公平模式的“公平”不是指线程优先级或响应时间均等,而是严格按调用 acquire() 的时序入队和出队。关键判断在 FairSync.tryAcquireShared() 中反复调用的 hasQueuedPredecessors() ——它检查当前线程是否是同步队列头节点的后继者。只要队列非空(哪怕只是个虚拟头节点),新线程就放弃直接竞争,直接入队。
这意味着:即使此时有空闲许可,只要前面有人在等,新线程也得老老实实排后面。非公平模式则跳过这步,先 CAS 尝试抢一次再说。
-
hasQueuedPredecessors()返回true→ 当前线程必须入队,不抢 - 返回
false→ 才会进入getState()和compareAndSetState()竞争流程 - 这个检查发生在每次
acquire()调用的最开始,不是只在首次失败后补查
tryAcquireShared() 的自旋逻辑与 state 更新不可分割
AQS 共享模式下,tryAcquireShared() 必须返回一个整数:负值表示失败并入队,0 表示成功但无需唤醒后继,正值表示成功且可唤醒多个后继。公平模式的实现里,这个返回值直接来自 available - acquires 的计算结果,而 available 是 getState() 的瞬时快照。
所以即便多个线程几乎同时到达,它们看到的 state 值可能一致,但只有第一个成功 CAS 修改成功的线程能继续;其余全部失败并入队 —— 这就是 AQS 用 CAS + 队列组合解决“竞态窗口”的核心机制。
- 返回负值(如
-1)→ 线程被挂起,加入 AQS 同步队列尾部 - 返回正值(如
2)→ 当前线程获取成功,且后续等待者可能被批量唤醒 - 没有锁住整个临界区,全靠
compareAndSetState()的原子性保一致性
公平模式不提升吞吐量,但让 acquire() 延迟更可预测
公平模式的代价是上下文切换更频繁、平均延迟略高:每个被拒的线程都要走一遍入队 → 挂起 → 被唤醒 → 再次尝试的路径。而非公平模式下,刚释放许可的线程可能立刻被新到线程“截胡”,省掉一次挂起/唤醒开销。
但在资源长期紧张(比如许可数远小于并发线程数)的场景下,公平模式能避免某些线程饿死 —— 因为它的等待权重完全由入队时间决定,跟线程调度器偏好、CPU 时间片分配无关。
- 高负载下,非公平模式可能出现某线程连续获取多次,而另一线程卡在队尾几十秒
- 公平模式下,第 N 个调用
acquire()的线程,大概率就是第 N 个拿到许可的线程 - 注意:
release()操作本身不区分公平/非公平,它总是唤醒队列头部节点
别把 new Semaphore(n, true) 当成“绝对公平”的银弹
构造时传 true 只影响 acquire() 的排队策略,不影响其他行为:比如 tryAcquire(long timeout, TimeUnit unit) 在超时前仍会按公平逻辑排队;drainPermits() 和 reducePermits() 也不受公平性开关控制;更关键的是,JVM 线程调度本身就不保证实时性 —— 即便你排在队头,OS 也可能暂时不给你 CPU 时间片。
真正影响“感知公平性”的,往往是上层业务逻辑:比如一个线程 acquire() 成功后做了 5 秒 IO,另一个线程在它释放前又调了 10 次 acquire(),那这 10 次都会被塞进队列,但它们之间仍是 FIFO,不会因为第 10 次调用更“急迫”就被插队。
- 公平模式只管“谁先来”,不管“谁更需要”或“谁等得更久”
- 如果业务需要按优先级或 SLA 分配许可,得自己在 Semaphore 外包一层调度逻辑
-
acquireUninterruptibly()在公平模式下依然遵循排队顺序,但会忽略中断信号
公平模式的源码路径非常干净,它没引入额外状态或分支,纯粹靠 AQS 队列 + 一次前置检查完成语义约束。容易被忽略的一点是:hasQueuedPredecessors() 的实现本身不加锁、不阻塞,但它依赖 AQS 内部 head 和 tail 指针的 volatile 语义 —— 这才是公平性得以跨线程成立的底层基础。










