孤舟笔记 并发篇十 ReentrantLock的公平锁和非公平锁是怎么实现的?这个设计藏着大智慧

文章目录

个人网站

你知道 new ReentrantLock(true)new ReentrantLock(false) 的区别吗?一个参数,天壤之别。一个是"先来先得",一个是"谁能抢到归谁"。面试官最爱问的就是:公平锁和非公平锁底层到底怎么实现的?为什么默认是非公平锁?

今天咱们就把这个设计拆开,看看里面藏着什么智慧。

先说结论:公平 vs 非公平

维度 公平锁(FairSync) 非公平锁(NonfairSync)
构造参数 new ReentrantLock(true) new ReentrantLock(false)(默认)
加锁策略 先检查队列,队列为空才 CAS 直接 CAS 抢锁,不管队列
吞吐量 较低(线程切换频繁) 较高(减少线程切换)
饥饿问题 不会饥饿 可能饥饿(等待线程可能一直抢不到)
实现差异 tryAcquire 多了 hasQueuedPredecessors 检查 tryAcquire 直接 CAS

一句话记住:公平锁像排队取号,非公平锁像抢红包------排队讲秩序,抢红包讲手速。

非公平锁:先抢再说,抢不到再排队

非公平锁的加锁流程特别"粗暴":

java 复制代码
// NonfairSync.lock()
final void lock() {
    if (compareAndSetState(0, 1))           // 第1步:直接 CAS 抢锁 👈
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);                          // 第2步:抢不到再走 AQS 排队
}

不看队列,不管有没有人在等,上来就抢。 抢到了直接用,抢不到再老老实实排队。

更关键的是,在 tryAcquire 里也直接抢:

java 复制代码
// NonfairSync.tryAcquire(调用 nonfairTryAcquire)
final boolean nonfairTryAcquire(int acquires) {
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {  // 直接 CAS,不检查队列 👈
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
    } else if (getExclusiveOwnerThread() == Thread.currentThread()) {
        setState(c + acquires);  // 可重入
        return true;
    }
    return false;
}

生活类比: 你去银行办业务,非公平锁就是------不管大厅里有没有人在等号,你冲到柜台就看能不能办。柜台空着就办了,排队的人?他们继续等。

公平锁:先看队列,有人排队就别插队

公平锁的加锁流程多了一步检查

java 复制代码
// FairSync.lock()
final void lock() {
    acquire(1);  // 没有直接 CAS 抢锁!直接进 AQS 流程 👈
}

// FairSync.tryAcquire
protected final boolean tryAcquire(int acquires) {
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&        // 关键!检查队列有没有人等 👈
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
    } else if (getExclusiveOwnerThread() == Thread.currentThread()) {
        setState(c + acquires);
        return true;
    }
    return false;
}

hasQueuedPredecessors() 是公平锁的灵魂。它检查 CLH 队列中是否还有比当前线程更早等待的节点。如果有,即使 state == 0,也放弃抢锁,乖乖去排队。

生活类比: 还是银行,公平锁就是------你到柜台前,先看大厅有没有人在等号。有人在等,你就去取号排队;没人等,你直接办。先来先得,绝不插队。

一行代码的差异:hasQueuedPredecessors

公平锁和非公平锁的核心差异,就在 tryAcquire 里的这一行:

java 复制代码
// 公平锁:多了这个检查
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires))
// 非公平锁:没有这个检查
if (compareAndSetState(0, acquires))

hasQueuedPredecessors() 的实现:

java 复制代码
public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
// head != tail → 队列不空
// h.next.thread != 当前线程 → 队首等的人不是我 👈

就这一行检查,决定了"能不能插队"。

为什么默认是非公平锁?

这是一个经典的设计权衡:

非公平锁吞吐量更高。 原因是:当锁释放时,唤醒队列中的线程需要时间(线程从 park 恢复到运行)。在这段"窗口期"内,如果新线程直接 CAS 抢到了锁,就省去了线程切换的开销。

公平锁保证不饥饿。 但代价是每次释放锁都要等队列中的线程恢复,线程切换频繁,整体吞吐量下降。

实测数据:在中等竞争场景下,非公平锁的吞吐量比公平锁高 数十倍 。所以 ReentrantLock 默认用非公平锁------性能优先,大多数场景不需要严格公平

公平锁与非公平锁全景

复制代码
公平锁与非公平锁 全景

核心差异
├── 非公平锁(NonfairSync)
│   ├── lock() 直接 CAS 抢锁
│   ├── tryAcquire 不检查队列
│   └── 可能"插队"导致饥饿
└── 公平锁(FairSync)
    ├── lock() 直接进 AQS 排队
    ├── tryAcquire 检查 hasQueuedPredecessors
    └── 严格 FIFO,不会饥饿

hasQueuedPredecessors
├── head != tail → 队列不空
└── h.next.thread != 当前线程 → 不是队首 → 排队去

选择建议
├── 默认非公平(性能优先)
├── 需要严格 FIFO → 公平锁
└── 饥饿敏感场景 → 公平锁

口诀:非公平直接抢,公平先看队列长,
      hasQueuedPredecessors 是关键,吞吐公平要权衡。

回答技巧与点评

标准回答

ReentrantLock 的公平锁和非公平锁通过两个内部类 FairSync 和 NonfairSync 实现。核心差异在 tryAcquire 方法:公平锁在 CAS 抢锁前会调用 hasQueuedPredecessors() 检查 CLH 队列是否有更早等待的线程,有则放弃抢锁去排队;非公平锁直接 CAS 抢锁,不检查队列。默认使用非公平锁,因为非公平锁在锁释放的"窗口期"允许新线程直接获取锁,减少线程切换开销,吞吐量更高。

加分回答
  1. 设计哲学:公平锁体现了"正义优先"------先来先得,但代价是性能;非公平锁体现了"效率优先"------整体吞吐更高,但可能牺牲个别线程的等待时间。这是典型的公平性与效率的权衡
  2. 边界情况:非公平锁在极端情况下可能导致"饥饿"------某线程反复被插队,长时间获取不到锁。但在实际应用中极少发生,因为线程调度本身就有随机性。如果业务确实要求严格有序,才需要公平锁
  3. 实际应用:ZooKeeper 的 Leader 选举用公平锁保证顺序;Kafka 的消费组再均衡也考虑公平性。而大多数 Web 服务器、数据库连接池用非公平锁,追求高吞吐
面试官点评

这道题考的是你对锁设计的权衡思维和源码级理解 。只说"公平锁排队、非公平锁插队"太表面。能讲出 hasQueuedPredecessors 这个关键方法、非公平锁吞吐量更高的原因(线程唤醒窗口期),以及为什么不默认公平锁,才是面试官想听到的。如果你能延伸到实际框架中的选择,说明你有工程视角。

原文阅读


内容有帮助?点赞、收藏、关注三连!评论区等你 💪

相关推荐
逻辑驱动的ken1 天前
Java高频面试考点场景题17
开发语言·jvm·面试·求职招聘·春招
逻辑驱动的ken4 天前
Java高频面试考点场景题14
java·开发语言·深度学习·面试·职场和发展·求职招聘·春招
实习僧企业版6 天前
如何为中小企业点亮校招吸引力的灯塔
大数据·春招·雇主品牌·招聘技巧·口碑
逻辑驱动的ken6 天前
Java高频面试考点场景题13
java·开发语言·jvm·面试·求职招聘·春招
Javatutouhouduan7 天前
阿里2026最新Java面试核心讲(终极版)
java·java面试·java并发·后端开发·java程序员·java八股文·java性能优化
逻辑驱动的ken9 天前
Java高频面试考点场景题10
java·开发语言·深度学习·求职招聘·春招
逻辑驱动的ken11 天前
Java高频面试考点场景题08
java·开发语言·面试·求职招聘·春招
逻辑驱动的ken12 天前
Java高频面试场景题07
java·开发语言·面试·职场和发展·求职招聘·春招
逻辑驱动的ken14 天前
Java高频面试考点场景题05
java·开发语言·深度学习·求职招聘·春招