公平锁与非公平锁

一、概念定义

公平锁(Fair Lock)

公平锁指多个线程按照请求锁的顺序(FIFO)依次获取锁。先到先得,排队等待。

ReentrantLock fairLock = new ReentrantLock(true); // true = 公平策略

非公平锁(Unfair Lock / Nonfair Lock)

非公平锁允许线程插队——新到达的线程会先尝试抢占锁一次,抢占失败才进入等待队列。

ReentrantLock unfairLock = new ReentrantLock(false); // false = 非公平(默认)
// 等价于:
ReentrantLock defaultLock = new ReentrantLock(); // 默认是非公平锁

核心对比

特性公平锁非公平锁
线程调度FIFO 排队,严格按请求顺序允许插队,可能”后发先至”
吞吐量低(上下文切换频繁)(默认选择,性能更优)
公平性✅ 保证顺序❌ 可能导致线程饥饿(极低概率)
适用场景对响应时间敏感、需要公平调度大多数生产环境
默认策略ReentrantLock(false)、synchronized两者默认均为非公平

默认锁: 无论是 synchronized 还是 ReentrantLock,默认都是非公平锁(性能优于公平锁)。


二、底层实现原理

2.1 AQS(AbstractQueuedSynchronizer)与等待队列

公平锁和非公平锁的实现基础是 AQS(AbstractQueuedSynchronizer),它内部维护了一个 CLH 变体双向队列(FIFO):

AQS 内部结构:
┌─────────────────────────────────────────────────────────────┐
│  AbstractQueuedSynchronizer                                  │
│  ┌──────────┐   ┌──────┐   ┌──────┐   ┌──────┐   ┌──────┐  │
│  │  state   │   │ Node │←→│ Node │←→│ Node │←→│ Node │  │
│  │ (锁状态)  │   │(head)│   │      │   │      │   │(tail)│  │
│  └──────────┘   └──────┘   └──────┘   └──────┘   └──────┘  │
│  ┌──────────┐                                              │
│  │ exclusive│  ← 当前持有锁的线程                           │
│  │OwnerThread│                                             │
│  └──────────┘                                              │
└─────────────────────────────────────────────────────────────┘

2.2 公平锁加锁流程 FairSync#tryAcquire

公平锁的关键在于 hasQueuedPredecessors() 判断——如果有线程在队列中等待更久,当前线程就不能抢锁

// ReentrantLock.FairSync 源码核心(JDK 17)
static final class FairSync extends Sync {
    @ReservedStackAccess
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        
        if (c == 0) {  // 锁未被持有
            // ★ 关键:检查等待队列中是否有前驱节点
            if (!hasQueuedPredecessors() &&  // 没有等待更久的线程
                compareAndSetState(0, acquires)) {  // CAS 尝试获取
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {  // 重入
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

hasQueuedPredecessors() 的实现:

// hasQueuedPredecessors: 判断是否有线程比当前线程等待更久
public final boolean hasQueuedPredecessors() {
    Node h = head;
    Node t = tail;
    Node s;
    // 队列不为空 且 头节点的后继节点不是当前线程
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

2.3 非公平锁加锁流程 NonfairSync#tryAcquire

非公平锁在锁空闲时直接尝试 CAS 抢锁,不会检查等待队列

// ReentrantLock.NonfairSync 源码核心(JDK 17)
static final class NonfairSync extends Sync {
    @ReservedStackAccess
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}
 
// 父类 Sync 中的实现
abstract static class Sync extends AbstractQueuedSynchronizer {
    @ReservedStackAccess
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        
        if (c == 0) {  // 锁未被持有
            // ★ 直接 CAS 抢锁,不检查等待队列!
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {  // 重入
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

2.4 加锁流程对比图

公平锁(FairSync):
线程请求锁
    │
    ▼
┌──────────────────────┐
│  锁状态检查           │
│  state == 0 ?        │
└───────┬──────────────┘
        │
   ┌────┴────┐
   │ state=0  │ ← 锁空闲
   └────┬────┘
        │
        ▼
┌──────────────────────┐
│ hasQueuedPredecessors│ ← ★ 检查等待队列
│ = false?             │ ─── 有前驱 → 排队去
└───────┬──────────────┘
        │ true(无前驱)
        ▼
┌──────────────────────┐
│  CAS 抢锁             │
└──────────────────────┘


非公平锁(NonfairSync):
线程请求锁
    │
    ▼
┌──────────────────────┐
│  锁状态检查           │
│  state == 0 ?        │
└───────┬──────────────┘
        │
   ┌────┴────┐
   │ state=0  │ ← 锁空闲
   └────┬────┘
        │
        ▼
┌──────────────────────┐
│  直接 CAS 抢锁        │ ← ★ 不检查队列,直接抢
│  compareAndSetState  │ ─── 失败 → 才进入等待队列
└──────────────────────┘


非公平锁的"插队"过程:
时间轴:  T1  T2  T3  T4  T5
状态:   持有锁  排  排  排  ← T5 是新来的线程
                 队  队  队
                 等  等  等
                 待  待  待

T5 持有锁的线程释放锁瞬间:
  ┌ 排队线程(T2/T3/T4)需要从等待队列被唤醒(上下文切换)
  └ T5(新线程)直接 CAS 抢锁 → 成功 → 插队成功!

★ 核心差异:
  非公平锁给了新线程一次"插队"机会,失败才排队。
  这个过程减少了线程挂起/唤醒的开销,提升吞吐量。

三、性能对比分析

3.1 为什么非公平锁性能更好?

非公平锁的优势来源于减少了线程唤醒的开销

  1. 线程唤醒成本高:等待队列中的线程被唤醒需要操作系统进行上下文切换(内核态)
  2. 插队减少切换:新线程通过 CAS 直接抢到锁,避免了排队线程的唤醒开销
  3. 可接受的不公平:插队成功概率适中,长期来看不至于导致线程饥饿(JVM 实际测试验证)
非公平锁的"插队"带来的性能优势:

时间线:
Thread A 释放锁
    │
    ├── 排队线程 Thread B:被操作系统唤醒(慢,几微秒)
    └── 新线程 Thread C:CAS 直接抢锁(快,纳秒级)
                        ↓
                   Thread C 插队成功
                        ↓
                   当 Thread C 释放锁时:
                   Thread B 刚好被唤醒 → 拿到锁
                   Thread C 已经做完工作
                   → 整体吞吐量提升!

3.2 公平锁 vs 非公平锁性能对比

指标公平锁非公平锁差距
吞吐量非公平锁通常高出 1~2 个数量级
上下文切换频率公平锁切换次数多得多
线程饥饿风险极低(实际几乎不会发生)
响应时间波动稳定有波动(可能有线程被连续插队)

四、synchronized 的公平性

synchronized 作为 Java 内置锁,默认且只能是非公平的

// synchronized 无法指定公平策略,永远是非公平锁
public synchronized void method() {
    // 不能设置为公平
}

synchronized 的锁升级机制与公平性:

锁状态公平性表现说明
偏向锁不涉及竞争单线程访问,无公平性概念
轻量级锁非公平自旋+CAS,谁先抢到谁执行
重量级锁非公平ObjectMonitor 的 _EntryList 本身不是严格 FIFO

虽然重量级锁底层使用队列,但本质上仍是非公平的——因为锁释放时,ObjectMonitor 的实现并不保证严格按入队顺序唤醒。


五、使用场景与选型建议

优先使用非公平锁的场景

  • 大多数生产环境——默认选择,性能更优
  • ✅ 对吞吐量要求高的场景
  • ✅ 锁持有时间短、竞争不激烈的场景
  • ✅ 可以接受短时间的线程调度不公平

使用公平锁的场景

  • 绝大多数情况下不需要
  • 真正需要公平调度的场景极其少见
  • ⚠️ 注意:公平锁保证的是获取锁的顺序,而非线程执行的总顺序
  • 如果业务逻辑对线程执行顺序有严格依赖,应考虑其他更合适的机制(如 SemaphoreCompletableFuture
// 公平锁使用示例(如果你真的需要)
private final ReentrantLock fairLock = new ReentrantLock(true);
 
public void execute() {
    fairLock.lock();
    try {
        // 任务处理
        System.out.println(Thread.currentThread().getName() + " 获得锁");
    } finally {
        fairLock.unlock();
    }
}

选型决策树

需要锁吗?
    │
    ├── 简单同步场景 → synchronized(非公平,简洁)
    │
    ├── 需要高级功能(超时/可中断/多条件)→ ReentrantLock
    │       │
    │       ├── 默认:非公平锁 new ReentrantLock() ✅ 推荐
    │       │
    │       └── 特殊情况:公平锁 new ReentrantLock(true)
    │            ├── 极其罕见的公平性要求
    │            └── 能接受性能下降
    │
    └── 读多写少场景 → ReentrantReadWriteLock(默认非公平)

六、总结

公平锁 vs 非公平锁 核心要点:
├── 公平锁:按请求顺序(FIFO)排队获取锁,避免饥饿
├── 非公平锁:允许新线程插队,提高整体吞吐量
├── 默认选择:synchronized 和 ReentrantLock 都默认非公平
├── 实现原理:AQS 的 hasQueuedPredecessors() 是区分关键
│   ├── 公平锁:获取前检查队列是否有前驱节点
│   └── 非公平锁:直接 CAS 尝试,失败才排队
├── 性能差异:非公平锁通过减少线程唤醒开销,吞吐量显著优于公平锁
└── 公平锁选型:绝大多数场景用非公平锁,极少情况需要公平锁

参考链接