好 — 我把公平锁(new ReentrantLock(true)的执行链路像之前非公平锁那样按调用顺序把相关源码段列出来并逐步讲解,重点指出与非公平锁的差异、hasQueuedPredecessors() 的实现含义,以及公平锁的优缺点与适用场景。目标是让你能在源码里快速定位「为什么公平锁不会跳队」这一点,并理解它的性能权衡。


一句话先概览

  • 公平锁(Fair ReentrantLock)在尝试获取锁时,会尊重队列顺序:如果有其他线程在先排队(有前驱),当前线程不会直接抢占锁(即便 state==0),而是加入等待队列按序竞争。
  • 技术上:公平实现通过在 tryAcquire 里调用 hasQueuedPredecessors() 来判断是否有排队前驱,从而决定是否进行 CAS 获得锁。

一、执行链路(调用顺序 + 关键源码片段 + 行为解析)

下面展示典型路径:线程 T 在公平锁上调用 lock(),然后可能进入 AQS 排队并等待,最后 unlock() 唤醒后继。

1) ReentrantLock#lock()(入口)

public void lock() {
    sync.lock();
}
  • sync 在构造 new ReentrantLock(true) 时被设置为 new FairSync()

2) ReentrantLock.FairSync#lock()(公平实现)

static final class FairSync extends Sync {
    final void lock() {
        acquire(1);
    }
}
  • 关键区别:非公平实现尝试先用 CAS 抢占(compareAndSetState(0,1)),而公平实现直接调用 acquire(1),不先做无队列快抢占(因此它更“守秩序”)。

3) AbstractQueuedSynchronizer#acquire(int arg)(模板)

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  • 与非公平路径相同:先调用 tryAcquire(arg)(由 Sync 的实现覆盖),如果 tryAcquire 返回 false,就入队等待。

4) ReentrantLock.Sync#tryAcquire(int acquires)(公平与非公平的不同点)

公平与非公平的 Sync 都继承自 AbstractQueuedSynchronizer,但是 SynctryAcquire 在公平与非公平中略有不同。下面给出公平版本的tryAcquire(简化):

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 公平性检查:如果队列中有排队的前驱,则不能直接抢占
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        setState(nextc);
        return true;
    }
    return false;
}

说明与要点

  • c == 0(锁空闲)时:

    • 公平实现会先调用 hasQueuedPredecessors()

      • 有前驱(即有人先排队),则直接返回 false,当前线程不会去 CAS 抢锁。
      • 没有前驱,才尝试 compareAndSetState(0, acquires) 抢占。
  • 当当前线程已是 owner(重入)时,允许重入。

  • 这就是公平锁“不给后来者插队”的实现核心。


5) AbstractQueuedSynchronizer#hasQueuedPredecessors() 的源码与含义

hasQueuedPredecessors() 是判断当前线程是否有排队前驱的关键方法。JDK8 中的典型实现(简化)如下:

public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    // 如果 head != tail(队列里有人)且 head.next == null 或 head.next.thread != current,
    // 则说明当前线程不是队首(有前驱)
    return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}

解释

  • h != t:队列不为空(存在等待节点)。如果 head==tail,说明队列空或只有虚拟头节点,当前线程无前驱。
  • s = h.next:队列第一个实际节点(队首等待者)。
  • 如果 s == null(很短暂的中间状态)或 s.thread != current(队首不是当前线程),则说明当前线程不是队首,即存在前驱线程或其他线程早来排队。
  • 返回 true 表示有前驱(不能插队),返回 false 表示没有前驱(可以尝试 CAS 获取锁)。

注意

  • 这个判断尽量轻量,不遍历整个队列,仅通过 head.next 来判断是否当前线程为首位。因为在公平策略下,我们只关心“是否有人排在我前面”,而不是具体有多少人。

6) 若 tryAcquire 返回 false → addWaiterenqacquireQueued(与非公平完全相同)

  • addWaiter(Node.EXCLUSIVE):把当前线程封装为 Node,尝试快速尾插(CAS tail),失败则调用 enq(node) 自旋入队。
  • acquireQueued(node, arg):队列等待循环 —— shouldParkAfterFailedAcquireparkAndCheckInterrupt 等机制与非公平锁相同。
    (详见你之前已阅读的 acquireQueued 逐行讲解)

7) unlock() 与释放流程(与非公平相同)

释放时调用的是 AQS 的模板 release(1),ReentrantLock 的 tryRelease(1) 也是一样的逻辑(减 statestate==0 则清 owner 并返回 true),AQS 在完全释放时会调用 unparkSuccessor(head) 唤醒队列中的下一个等待者(FIFO)。


二、公平锁与非公平锁的关键对比(源码角度总结)

维度 非公平(NonfairSync) 公平(FairSync)
lock() 的实现 先尝试 compareAndSetState(0,1)(快抢),失败再 acquire(1) 直接 acquire(1)(不先快抢)
tryAcquire 中是否检查队列 不检查(或非公平实现的 tryAcquire 在快速路径里不调用 hasQueuedPredecessors() 使用 hasQueuedPredecessors() 来判断是否有前驱,若有前驱则不抢占
插队可能性 允许插队(后来线程可能成功 CAS 抢到锁,即使有人在队列中) 不允许插队(尊重先到先得)
性能开销 更高吞吐,低延迟(尤其在低争用下) 延迟更大,吞吐可能更低(因过多的队列检查与更严格调度)
适用场景 常用默认,绝大多数业务 需要严格公平性保证的场景(少见)

三、hasQueuedPredecessors() 的微妙之处(为什么实现是那样)

你可能好奇:为什么 hasQueuedPredecessors() 只看 head.next 而不遍历整个队列?原因是高并发下遍历会开销大;而只需判断当前线程是否就是队首即可决定是否允许插队。

考虑常见情形:

  • 若队列空(head==tail),返回 false(无前驱);
  • 若队列不空但 head.next.thread == current,说明你实际上已经是队首(可能因为之前你刚入队并被快速设为 head.next),此时可以尝试获取;
  • 在极端竞态下 head.next 可能为 null(短暂性),实现上也将其视为“有前驱”,迫使线程排队稳定后再判断,避免抢占造成不公平。

四、示例时序对比(直观感受)

假设线程顺序为 T1 → T2 → T3 到达竞争锁,且锁目前空闲:

非公平锁

  • T1、T2、T3 同时到达:若 T3 比 T1/T2 更早做 CAS(极少见但可能),T3 可能直接成功拿到锁(插队成功),其余线程入队等待或再尝试。
  • 优点:可能更快(T3 直接拿到锁减少延迟)。
  • 缺点:不保证到达顺序,可能造成“饥饿”。

公平锁

  • T1 到达:hasQueuedPredecessors() 返回 false → T1 获取锁。
  • T2 到达:若 T1 未释放,hasQueuedPredecessors() 返回 true → T2 不会尝试抢占,直接入队。
  • T3 类似,入队尾。
  • 解锁后按队列顺序唤醒 T2、T3。
  • 优点:严格先到先得;缺点:吞吐率通常低于非公平。

五、何时选公平锁?何时不要用?

  • 多数情况下不建议使用公平锁(除非有非常明确的公平性需求),原因:

    • 性能牺牲:频繁调用 hasQueuedPredecessors() 并避免抢占会降低并发吞吐。
    • 线程切换开销:严格按队列唤醒可能导致更多上下文切换与调度延迟。
  • 适用场景:

    • 需要强公平保证(例如某些资源分配策略、计费/审计场景或避免长时间线程饥饿)。
    • 对响应顺序有硬性要求的业务(非常少见)。

六、补充:公平锁在实现上的“小优化点”和常见误解

  • 虽然公平锁不做“先快抢占”,但一旦某个线程成为队首并成功 tryAcquire,后续重入仍按普通规则走(当前 owner 可重入)。
  • 有人误以为“公平锁就一定更好”——实际上对于短时间持锁且高并发场景,非公平通常性能更佳且能减少延迟。
  • JDK 的 ReentrantLock 默认是非公平的(new ReentrantLock()),这是实践经验的妥协:大多数场景优先考虑吞吐和延迟。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐