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

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

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

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

面试官提出这个问题,通常意在考察以下几个核心维度:

  1. 对锁 “公平性” 概念的理解:候选人能否准确阐述公平锁和非公平锁在 “线程获取锁的顺序” 上的本质区别。
  2. 对性能与开销的权衡意识:面试官不仅仅是想知道定义,更是想知道候选人是否理解这两种策略在性能(吞吐量、延迟)和系统开销(上下文切换)上的差异及其原因。
  3. 底层实现机制的了解:候选人是否了解 ReentrantLock 等锁是如何利用 AQS(AbstractQueuedSynchronizer)的同步队列来实现这两种策略的。
  4. 实际场景的应用能力:考察候选人能否根据具体业务场景(如对响应时间要求极高、或需要严格按序执行)做出合理的选择,这反映了其工程实践经验。

核心答案

公平锁与非公平锁的核心区别在于 线程获取锁的调度策略

  • 公平锁:多个线程按照 申请锁的绝对时间顺序 来排队获取锁。锁释放后,会优先唤醒在同步队列中等待时间最长的线程(队头线程)。这保证了获取锁的 “先来后到”,是公平的。
  • 非公平锁:线程在尝试获取锁时,可以 “插队”。无论同步队列中是否有其他线程在等待,新来的线程都有机会直接尝试竞争锁。如果竞争失败,才会加入队列尾部排队。这种策略不保证申请锁的时间顺序。

在 Java 中,ReentrantLock 可以通过构造函数 new ReentrantLock(true) 创建公平锁,默认(或 new ReentrantLock(false))创建的是非公平锁。synchronized 关键字实现的锁是非公平锁

深度解析

原理/机制

其底层实现依赖于 AQS 内部的 CLH 变体同步队列

  • 公平锁的 tryAcquire 逻辑:在尝试获取锁时,会先检查 AQS 同步队列中是否有前驱节点在等待(hasQueuedPredecessors())。如果有,则当前线程自觉加入队列尾部排队,不会尝试竞争。这确保了 “先到先得”。
  • 非公平锁的 tryAcquire 逻辑:在尝试获取锁时,不会检查队列。无论队列是否为空,都会直接通过 CAS 操作去抢锁。这给了新线程“插队”的机会。

代码示例

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class FairAndUnfairLockDemo {
    public static void main(String[] args) {
        // 分别测试公平锁和非公平锁
        testLock(new ReentrantLock(true), “公平锁”);
        testLock(new ReentrantLock(false), “非公平锁”); // 默认
    }

    private static void testLock(Lock lock, String lockType) {
        System.out.println("\n=== 测试: " + lockType + " ===");
        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                for (int j = 0; j < 2; j++) { // 每个线程获取两次锁
                    lock.lock();
                    try {
                        System.out.println(Thread.currentThread().getName() + " 获得锁");
                        Thread.sleep(100); // 模拟持有锁一段时间
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        lock.unlock();
                    }
                }
            }, "Thread-" + i).start();
        }
        // 等待所有线程执行完毕
        try { Thread.sleep(3000); } catch (InterruptedException e) {}
    }
}

运行上述代码,你可以观察到:在公平锁模式下,输出中线程获取锁的顺序更接近其启动顺序,规律性强;而在非公平锁模式下,输出顺序可能混乱,经常出现一个线程连续获取锁的情况(“线程饥饿” 现象),这直观展示了 “插队” 行为。

对比分析与最佳实践

特性公平锁非公平锁
获取顺序严格按照 FIFO(先入先出)允许 “插队”,不保证顺序
吞吐量相对较低相对更高(通常是非公平锁的几倍)
线程饥饿不会发生可能发生
上下文切换较多,线程频繁排队/唤醒相对较少,新线程可能直接获取锁成功
  • 为何非公平锁性能通常更高? 核心原因在于 减少了线程的挂起和唤醒次数。当持有锁的线程释放锁时,如果恰好有一个新线程来请求,这个新线程可以立即获取锁并执行,避免了将自身挂起、再被唤醒的开销(这是一个耗时的内核态操作)。此外,这种“插队”机制也更好地利用了 CPU 的时间片,提升了整体吞吐量。
  • 最佳实践默认情况下,优先使用非公平锁。在 ReentrantLock 的文档中明确建议,除非对公平性有特殊要求,否则使用非公平锁能获得更高的吞吐量。只有在必须保证线程执行的严格顺序(例如,防止低优先级线程长期无法执行),且能接受一定性能损耗的场景下,才考虑使用公平锁。

常见误区

  • 误区一:非公平锁不公平,所以不好。 这是一种教条的理解。在绝大多数并发场景下,“不公平” 带来的性能收益远大于其理论上的缺点。系统设计是在做权衡(Trade-off)。
  • 误区二:synchronized 是公平锁。 不对,synchronized 的实现是典型的非公平锁,它不提供任何公平性保证。
  • 误区三:公平锁能完全解决线程饥饿。 虽然公平锁在锁的获取层面解决了饥饿,但如果线程在临界区内执行时间过长,依然会影响整个系统的响应性。

总结

简单来说,公平锁讲究秩序,性能有代价;非公平锁鼓励竞争,吞吐量更高。在实际开发中,非公平锁是更普适和高效的选择,而公平锁仅适用于对线程执行顺序有严格要求的特定场景。