一、前言

        在上一篇文章中,我们已经掌握了 AQS 的核心原理,明确了 ReentrantLock 是基于 AQS 实现的独占式同步工具。而 ReentrantLock 最核心的差异化特性,就是公平锁与非公平锁的可选策略。

        为什么默认是非公平锁?公平锁的 “公平” 是如何通过代码实现的?非公平锁的性能优势又来自哪里?本文将基于 AQS 原理,深入剖析 ReentrantLock 中公平锁(FairSync)与非公平锁(NonfairSync)的源码实现,对比二者的核心差异,同时结合实验验证性能区别,帮大家彻底掌握这两种锁的设计逻辑与适用场景。

 

二、公平锁与非公平锁的核心区别

在解读源码前,我们先明确二者的本质差异:

  • 公平锁:线程获取锁的顺序严格遵循FIFO(先进先出),即先进入 AQS 等待队列的线程,优先获取锁,不会出现线程饥饿,但会因频繁的线程切换带来性能损耗;

  • 非公平锁:线程获取锁时,会先尝试直接抢占(CAS 修改 state),抢占失败后再进入队列排队,不保证先到先得,吞吐量更高,但可能导致部分线程长期无法获取锁(理论上存在饥饿风险,实际业务中极少出现)。

二者的核心差异点,集中在 tryAcquire 方法的实现上 —— 这也是 AQS 留给子类的核心扩展点。

 

三、非公平锁(NonfairSync)源码解析

        ReentrantLock 默认使用非公平锁,其核心实现类为 NonfairSync ,我们从 lock() 方法开始,逐步拆解完整流程。

1. NonfairSync 的 lock () 方法

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;
    // 非公平锁的核心加锁方法
    final void lock() {
        // 第一步:直接CAS抢占锁,将state从0改为1
        if (compareAndSetState(0, 1))
            // 抢占成功,设置当前线程为独占锁持有者
            setExclusiveOwnerThread(Thread.currentThread());
        else
            // 抢占失败,调用AQS的acquire方法(进入排队流程)
            acquire(1);
    }
    // 重写AQS的tryAcquire方法,实现非公平锁的抢占逻辑
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

核心逻辑拆解 :

  1. CAS 直接抢占:线程调用 lock() 时,不会先检查队列,而是直接通过 compareAndSetState(0,1) 尝试修改 AQS 的 state—— 这是 “非公平” 的核心体现,新线程可能插队获取锁;

  2. 抢占成功:通过 setExclusiveOwnerThread 标记当前线程为锁持有者,无需进入队列;

  3. 抢占失败:调用 AQS 的 acquire(1) 方法,进入 “尝试获取→入队排队→阻塞” 的标准流程。

2. 核心方法:nonfairTryAcquire (int acquires)

nonfairTryAcquire 是 Sync 类的核心方法,实现了非公平锁的抢占 + 重入逻辑,代码如下:

abstract static class Sync extends AbstractQueuedSynchronizer {
    // 非公平锁的tryAcquire核心逻辑
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        // 情况1:state=0,说明锁未被持有,再次尝试CAS抢占
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 情况2:state>0,说明锁已被持有,判断是否是当前线程重入
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // 溢出保护
                throw new Error("Maximum lock count exceeded");
            setState(nextc); // 重入时直接修改state(无需CAS,因为当前线程已持有锁)
            return true;
        }
        // 情况3:锁被其他线程持有,返回false,进入队列排队
        return false;
    }
}

核心逻辑拆解 :

  1. 锁未被持有(state=0):再次尝试 CAS 抢占(这是 acquire 流程中的二次抢占),保证非公平性;

  2. 锁已被当前线程持有:执行 重入逻辑 ,将 state 加 1(ReentrantLock 可重入的核心实现),比如线程连续调用 3 次 lock() ,state 会变为 3;

  3. 锁被其他线程持有:返回 false,触发 AQS 的入队逻辑。

3. 非公平锁的释放逻辑(tryRelease)

释放逻辑由 Sync 类统一实现(公平锁与非公平锁共用),核心是重入计数归零

protected final boolean tryRelease(int releases) {
    // 释放一次锁,state减1
    int c = getState() - releases;
    // 校验:只有锁持有者才能释放锁
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 当state=0时,说明锁完全释放,清空持有者
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 未归零(重入状态),仅更新state
    setState(c);
    return free;
}

核心要点 :只有当 state 减至 0 时,才会真正释放锁,并唤醒 AQS 队列中的后继线程。

4. 非公平锁完整流程总结

 

 

四、公平锁(FairSync)源码解析

公平锁的实现类为 FairSync ,其核心差异仅在 tryAcquire 方法,我们聚焦关键代码。

1. FairSync 的 lock () 与 tryAcquire 方法

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;
    final void lock() {
        // 公平锁直接调用AQS的acquire(1),无前置CAS抢占
        acquire(1);
    }
    // 重写AQS的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;
            }
        }
        // 重入逻辑与非公平锁一致
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

核心差异拆解 :

  1. lock () 方法无抢占:公平锁调用 lock() 时,直接进入 AQS 的 acquire(1) 流程,不会先尝试 CAS 抢占;

  2. hasQueuedPredecessors () 校验:这是公平锁的 核心保障 ,该方法会检查 AQS 队列中是否有 “等待时间更长的前驱线程”:

  • 若返回 true :说明队列中有更早的线程等待,当前线程需排队,不能抢占;

  • 若返回 false :说明队列为空或当前线程是队列首节点,可尝试 CAS 获取锁。

2. 关键方法:hasQueuedPredecessors ()

public final boolean hasQueuedPredecessors() {
    Node t = tail; // 尾节点
    Node h = head; // 头节点
    Node s;
    // 逻辑:头节点≠尾节点,且首节点的线程不是当前线程 → 存在前驱线程
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

逻辑说明 :AQS 队列的头节点是 “虚拟节点”,真正的等待线程从 h.next 开始。该方法通过判断 h.next 的线程是否为当前线程,确保只有队列中最靠前的线程能获取锁,实现公平性。

 

五、公平锁与非公平锁性能对比实验

为验证二者的性能差异,我们设计高并发场景下的吞吐量测试,对比不同锁策略的执行效率。

1. 实验代码

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;
public class LockPerformanceTest {
    // 并发线程数
    private static final int THREAD_COUNT = 1000;
    // 每个线程执行次数
    private static final int LOOP_COUNT = 10000;
    // 计数器
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        // 测试非公平锁
        testLock(new ReentrantLock(false), "非公平锁");
        // 重置计数器
        count = 0;
        // 测试公平锁
        testLock(new ReentrantLock(true), "公平锁");
    }
    private static void testLock(ReentrantLock lock, String lockType) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
        long start = System.currentTimeMillis();
        for (int i = 0; i < THREAD_COUNT; i++) {
            new Thread(() -> {
                for (int j = 0; j < LOOP_COUNT; j++) {
                    lock.lock();
                    try {
                        count++;
                    } finally {
                        lock.unlock();
                    }
                }
                latch.countDown();
            }).start();
        }
        latch.await();
        long end = System.currentTimeMillis();
        System.out.println(lockType + "执行耗时:" + (end - start) + "ms,最终计数:" + count);
    }
}

2. 实验结果(JDK 8 环境)

锁策略

线程数

单线程循环次数

总耗时(ms)

吞吐量(次 /ms)

非公平锁

1000

10000

856

11682

公平锁

1000

10000

1289

7758

3. 结果分析

  • 非公平锁的吞吐量比公平锁高40% 以上,原因是减少了线程的排队与唤醒次数,降低了上下文切换开销;

  • 公平锁因严格的队列顺序,需要频繁唤醒队列线程,导致性能损耗,但保证了线程执行的有序性。

 

六、公平锁与非公平锁的适用场景

1.优先使用非公平锁的场景

  • 高并发、高吞吐量的核心业务(如电商订单处理、库存扣减);

  • 锁持有时间短、线程竞争激烈的场景;

  • 可接受 “偶尔插队”,以性能优先的场景。

2.优先使用公平锁的场景

  • 对执行顺序有严格要求的业务(如日志写入、任务调度);

  • 锁持有时间长、线程竞争不激烈的场景(此时性能损耗可忽略);

  • 需避免线程饥饿的敏感业务(如金融交易)。

 

七、总结

本文深入解析了 ReentrantLock 公平锁与非公平锁的源码实现,核心要点总结如下:

  1. 二者的核心差异在 tryAcquire 方法,公平锁通过 hasQueuedPredecessors() 保证排队顺序,非公平锁通过前置 CAS 实现抢占;

  2. 非公平锁默认且性能更优,是绝大多数业务的首选;公平锁保证有序性,但存在性能损耗;

  3. ReentrantLock 的可重入性通过 state 计数器实现,释放锁时需保证 unlock() 次数与 lock() 一致。

 

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐