公平锁和非公平锁的区别?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 概念理解:面试官不仅仅是想知道 "公平锁排队、非公平锁插队" 这个表面区别,更是想知道你是否理解 "公平" 的代价是什么——线程上下文切换的开销。

  2. AQS 源码级理解:考察你是否看过 ReentrantLock 的源码,能否说清楚 FairSyncNonfairSync.tryAcquire() 方法中的具体差异。

  3. 工程选型意识:考察你是否知道 Java 默认用非公平锁,以及为什么绝大多数场景下非公平锁才是更好的选择。

核心答案

先甩结论:公平锁按线程等待顺序获取锁(先到先得),非公平锁允许新来的线程直接尝试 "插队" 获取锁,失败了再排队。Java 中 ReentrantLock 默认使用非公平锁,synchronized 也是非公平的。

对比维度 公平锁(FairSync 非公平锁(NonfairSync
获取顺序 严格按照 CLH 队列的等待顺序 新线程可直接 CAS 抢锁
吞吐量 较低(频繁上下文切换) 较高(减少线程挂起/唤醒)
饥饿问题 不会饥饿 可能饥饿(等待线程长期拿不到锁)
实现复杂度 需检查队列中是否有等待线程 直接 CAS,失败再入队
默认使用 是(ReentrantLock 默认非公平)

深度解析

一、用排队买票来类比

上面这个类比很直观:

  • 公平锁就是老老实实排队,先来后到,A 办完了一定是 B 上,没人能插队
  • 非公平锁允许刚到的线程直接去窗口试一把,万一窗口刚好空了,直接就办了,不用傻乎乎地先去队尾排队。如果没抢到,再去队尾老老实实排着

二、源码层面的差异

ReentrantLock 内部有两个内部类:FairSyncNonfairSync,它们都继承自 Sync(AQS 的子类)。关键差异在 .lock().tryAcquire() 方法上。

非公平锁的 .lock()

// NonfairSync.lock()
final void lock() {
    // 上来就 CAS 抢一把,不管队列里有没有人在等
    if (compareAndSetState(0, 1)) {
        setExclusiveOwnerThread(Thread.currentThread());
    } else {
        acquire(1);  // 抢不到再走正常流程
    }
}

公平锁的 .lock()

// FairSync.lock()
final void lock() {
    acquire(1);  // 直接走 acquire,没有 "先抢一把" 的机会
}

看到没?非公平锁一上来就 CAS 试着抢,抢到就赚了。公平锁不给你这个机会,直接进入 acquire() 流程。

再看 .tryAcquire() 的差异——这才是核心:

// NonfairSync 的 tryAcquire(调用 Sync.nonfairTryAcquire)
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // state == 0 说明锁空闲,直接 CAS 抢!不管队列
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 可重入逻辑...
    return false;
}

// FairSync 的 tryAcquire
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // ⬇️ 关键区别!多了 hasQueuedPredecessors() 检查
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 可重入逻辑...
    return false;
}

差异就一行代码:hasQueuedPredecessors()。这个方法检查 CLH 等待队列中是否有线程排在你前面。如果有,即使锁空闲你也不能拿,得老老实实排队。这就是 "公平" 的代价。

三、为什么默认非公平?

这个设计决策值得细说。很多人直觉上觉得 "公平肯定更好啊",但其实不然:

非公平锁吞吐量更高。 原因在于线程上下文切换的成本。假设线程 A 刚释放锁,此时队列中 B 正在等待。如果是公平锁,流程是:

A 释放锁 → 唤醒 B(内核态切换)→ B 从等待态恢复(内核态切换)→ B 获取锁

而非公平锁,如果恰好有个线程 C 正在 CPU 上运行:

A 释放锁 → C 直接 CAS 抢到(用户态,几乎无开销)

你看,非公平锁省掉了 "唤醒 B" 和 "B 恢复执行" 两次昂贵的上下文切换。JDK 的设计者经过测试,发现非公平锁的吞吐量比公平锁高出 一个数量级(参考 Doug Lea 的论文)。

当然,代价就是公平性——B 可能被饿死。但在绝大多数业务场景中,这点不公平完全可以接受。

四、什么时候用公平锁?

虽然非公平锁是默认选择,但以下场景确实需要公平锁:

  • 定时任务调度:任务执行顺序敏感,先提交的任务应该先执行
  • 资源分配:多个服务竞争共享资源,需要保证公平性
  • 限流/配额控制:不能让某些请求永远 "插队" 成功
// 创建公平锁
ReentrantLock fairLock = new ReentrantLock(true);  // true = 公平

// 创建非公平锁(默认)
ReentrantLock unfairLock = new ReentrantLock();       // 默认 false
ReentrantLock unfairLock2 = new ReentrantLock(false); // 显式指定

面试高频追问

  1. synchronized 是公平锁还是非公平锁?

    非公平锁。synchronized 底层基于 Monitor 实现,线程唤醒后谁先抢到 Monitor 全凭运气,不保证等待顺序。

  2. ReentrantReadWriteLock 的读锁和写锁是公平的吗?

    同样支持公平/非公平两种模式,通过构造函数参数控制。默认也是非公平的。但在公平模式下,读锁也不能插队——必须等队列中前面的写锁释放后才能获取。

  3. AQS 的 CLH 队列是什么?

    CLH(Craig, Landin, and Hagersten)队列是 AQS 内部维护的一个 双向链表,用来管理等待获取锁的线程。每个节点(Node)包含线程引用、等待状态等信息。公平锁就是严格按这个链表的 FIFO 顺序来调度线程。

常见面试变体

  • "为什么 ReentrantLock 默认用非公平锁?"
  • "公平锁有什么缺点?"
  • "synchronized 是公平锁吗?"
  • "AQS 是怎么实现公平锁的?"

记忆口诀

区别:公平排队不插队,非公平先抢再排队

选型:默认非公平求吞吐,业务需要公平再开启

源码差异:就差一个 hasQueuedPredecessors()(公平锁多了 "队里有没有人" 的检查)

总结

公平锁和非公平锁的核心区别在于获取锁时是否检查等待队列。公平锁通过 hasQueuedPredecessors() 保证先到先得,但代价是更多的上下文切换和更低的吞吐量;非公平锁允许新线程直接 CAS "插队",性能更好但可能导致线程饥饿。Java 默认非公平,这是 Doug Lea 经过充分测试后的工程决策。面试时把源码差异、为什么默认非公平、什么时候该用公平锁这三点讲清楚,基本满分。