文章目录

Java ReentrantLock的公平锁与非公平锁源码对比

发布于 2026-05-29 16:19:00 · 浏览 33 次 · 评论 0 条

Java ReentrantLock的公平锁与非公平锁源码对比

核心差异概述

ReentrantLock 是 Java 并发包中最常用的可重入锁。它内部通过一个 Sync 抽象类实现,并提供了两个具体子类:FairSync(公平锁)和 NonfairSync(非公平锁)。两者的核心区别在于线程获取锁的顺序:公平锁按照线程等待的先后顺序(FIFO)分配锁,非公平锁允许线程在尝试获取锁时“插队”。

本文从源码角度逐行对比,帮助你彻底理解两者的实现机制。


1. 从 lock() 方法开始

这是两种锁最直观的入口。

公平锁的 lock()

打开 ReentrantLock.java,找到 FairSync 内部的 lock() 方法:

static final class FairSync extends Sync {
    final void lock() {
        acquire(1);
    }
}

调用 acquire(1) 方法。acquireAbstractQueuedSynchronizer(AQS)的模板方法,它内部会依次:

  1. 尝试获取锁(调用 tryAcquire)。
  2. 若失败,将当前线程加入等待队列。

非公平锁的 lock()

对应 NonfairSynclock()

static final class NonfairSync extends Sync {
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }
}

关键区别:非公平锁在进入 acquire 逻辑前,先直接尝试一次 CAS 抢锁。如果成功,则不进入队列,直接获得锁,即使此时队列中已有等待线程。这就是“插队”的源头。


2. 核心方法 tryAcquire 对比

tryAcquire 是决定公平性的关键。两者都重写了 AQS 的 tryAcquire(int acquires)

公平锁的 tryAcquire

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
            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;
}

重点在于 hasQueuedPredecessors() 方法。它的作用是判断当前线程前面是否有其他等待线程。如果有,即使锁是空闲的,当前线程也不能抢锁,必须排队。

非公平锁的 tryAcquire

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        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;
}

注意:非公平锁的 tryAcquire没有调用 hasQueuedPredecessors()。当锁空闲(c == 0)时,它直接尝试 CAS 设置状态,成功即获得锁。这样,即使在等待队列头部有个线程已经等待了很久,新的线程仍可能抢锁成功。


3. hasQueuedPredecessors() 源码解析

这个方法是公平锁公平性的保障。打开 AbstractQueuedSynchronizer 查看:

public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

逻辑解释:

  • 如果队列为空(h == t),返回 false,表示没有前驱,可以抢锁。
  • 如果队列不为空,但头节点的下一个节点的线程就是当前线程,也返回 false,表示当前线程就是下一个需要服务的线程,允许抢锁。
  • 其他情况(队列中有等待线程,且当前线程不在队列头部)返回 true,表示有前驱,禁止抢锁。

公平锁正是通过这个方法强制所有线程按照入队顺序获取锁。


4. 对比总结表

对比维度 公平锁(FairSync) 非公平锁(NonfairSync)
lock() 入口 直接调用 acquire(1) 先尝试 CAS 抢一次锁,失败后调 acquire(1)
tryAcquire 中是否检查队列前驱 是,调用 hasQueuedPredecessors() 否,直接 CAS
线程获取锁顺序 FIFO(先来先服务) 可能插队(允许新线程比老线程先获得锁)
吞吐量表现 较公平,但上下文切换较多 通常更高,因为减少了线程挂起和唤醒
适用场景 对锁等待时间敏感,避免线程饥饿 追求高吞吐量,能容忍短暂的不公平

5. 性能与选择建议

非公平锁之所以“默认”被选择(ReentrantLock 的无参构造器默认创建非公平锁),是因为在大多数高并发场景下,允许插队能显著减少线程挂起和恢复的次数

  • 非公平锁:当一个线程释放锁时,正好有另一个新线程请求锁。新线程直接 CAS 成功,无需进入队列,也无需唤醒队列中的老线程。这避免了两个线程进行昂贵的上下文切换。
  • 公平锁:每次锁释放都必唤醒队列中第一个等待线程,哪怕此时有新线程请求,也必须乖乖入队。这会增加系统开销。

操作建议

  1. 优先使用非公平锁,除非你的业务明确要求锁的公平性(例如,必须按请求顺序执行任务)。
  2. 如果担心线程饥饿,非公平锁在高竞争下极少数线程可能长时间得不到锁;此时可以改用公平锁,或者控制锁持有的时间。

6. 完整代码结构回顾

ReentrantLock 内部类关系:

ReentrantLock
  └── Sync (抽象,继承AQS)
        ├── NonfairSync (非公平)
        └── FairSync (公平)
  • Sync 提供公共方法如 nonfairTryAcquire,但 NonfairSync 重写了 tryAcquire,覆盖了父类的实现。
  • 公平锁的 tryAcquire 完全自己实现,不调用父类的 nonfairTryAcquire

关键行为区别(用文字描述两个线程的竞争场景):

场景:锁被线程A持有,线程B和线程C同时等待。线程A释放锁的瞬间,线程D刚好调用 lock()

  • 非公平锁:线程D直接 CAS 尝试获取锁,成功则获得锁,线程B和线程C继续等待。
  • 公平锁:线程D发现队列中有等待线程(B或C),调用 hasQueuedPredecessors() 返回 true,因此即使锁空闲,线程D也只能入队排队,保证B或C先获得锁。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文