公平锁和非公平锁有什么区别?

#Java#面试八股

公平锁和非公平锁有什么区别?

结论先行:公平锁(fair lock)严格按请求到达顺序发放锁,先到先得;非公平锁允许新来的线程插队,只要锁刚好空出来就可能被它抢到。synchronized 只有非公平一种,ReentrantLock 可以通过构造参数选择。

1. 对比表

维度公平锁非公平锁
获取顺序严格 FIFO,按 AQS 队列顺序允许插队,抢到算赢
吞吐量较低较高
线程饥饿不会极端情况下可能
唤醒开销每次都要唤醒队首线程,上下文切换多刚释放的线程可能立刻被复用,省一次唤醒
实现开关new ReentrantLock(true)new ReentrantLock() 或 synchronized
典型场景对顺序敏感、要求等待时间可预期绝大多数业务场景

2. 差一行代码:hasQueuedPredecessors()

// 公平锁:先看队列里有没有人排队,有就让位
protected final boolean tryAcquire(int acquires) {
    if (getState() == 0
            && !hasQueuedPredecessors()          // ← 公平锁独有的判断
            && compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
    }
    return false;
}

// 非公平锁:不管有没有人排队,先 CAS 抢一把
protected final boolean tryAcquire(int acquires) {
    if (compareAndSetState(0, acquires)) {       // ← 抢到就是我的
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
    }
    return false;
}

公平与非公平的全部差别,就是这一个判断。

3. 插队是怎么发生的

公平锁:请求到达必须排到队尾
   [T1 持有] → [T2] → [T3] → [T4] ← 新来的 T5 只能排在 T4 后面

非公平锁:锁一释放,新来的可以直接抢
   [T1 持有] → [T2] → [T3] → [T4]
                            ↑ 此刻 T1 释放,T5 恰好到达并 CAS 成功
                              结果是 T5 先执行,T2/T3/T4 继续等

注意非公平不是完全不排队:抢失败时照样要进队列。它只是把"抢"的机会留给了刚到的线程。

4. 为什么非公平反而更快

关键在唤醒的延迟。锁释放后,队首线程要被唤醒、切换回 CPU,这段空窗期里锁是空闲的;非公平锁让一个正在运行的线程立刻抢到,等于把这段空窗填满了。

公平锁:  T1 释放 → 唤醒 T2(耗时几十微秒)→ T2 恢复运行 → 加锁
                   └─ 这段唤醒等待里,锁一直空闲 ─┘

非公平锁:T1 释放 → 旁边正在运行的 T5 直接 CAS 成功 → 立刻进入临界区

代价是队首线程可能被反复插队,产生饥饿(starvation),等待时间不可预期。

5. 常见追问

追问一:synchronized 是公平锁吗? 不是。synchronized 的 monitor 竞争完全由 JVM 和 OS 调度决定,不保证顺序,也无法设置成公平。要公平只能用 ReentrantLock(true)。

追问二:公平锁能保证严格顺序吗? 不能 100%。它保证的是"在锁上排队的顺序",但线程从被唤醒到真正执行之间仍可能被调度器插队;而且 tryLock() 是非公平的,无论锁是否公平设置,tryLock 都会直接尝试抢占。

追问三:非公平锁会导致死锁吗? 不会。插队只是顺序问题,不涉及循环等待。死锁需要"持有并等待 + 循环等待",与公平性无关。

追问四:怎么选? 默认用非公平(吞吐高)。只有对顺序敏感时才用公平——典型如按序处理同一账户的请求、任务调度器、需要避免长尾等待的资源池。如果只是担心饥饿,更好的办法是给临界区减负,而不是直接切公平锁。

小结

公平锁用吞吐换确定性,非公平锁用确定性换吞吐。ReentrantLock 的公平只是一个布尔参数,但背后是"是否检查队列"这一行判断——这也是 AQS 设计里最简洁的一处取舍。

笔记加载中…