Java公平锁 vs 非公平锁

公平锁和非公平锁是排他锁(独占锁) 的两种调度策略,核心区别是线程获取锁的顺序是否遵循 “先来先服务”ReentrantLock是 Java 中最典型的实现(通过构造方法指定公平性)。

核心定义

  • 公平锁:线程获取锁的顺序严格遵循FIFO(先进先出),等待时间最长的线程优先获取锁,不会出现 “插队” 现象;
  • 非公平锁:线程获取锁时不遵循 FIFO,新请求锁的线程可能 “插队” 优先获取锁(即使等待队列中有排队的线程),是 Java 中锁的默认实现(如ReentrantLock()无参构造、synchronized)。

实现原理

  1. 公平锁:线程请求锁时,先检查等待队列是否有排队线程,若有则直接加入队列尾部排队;只有队列头部的线程才能竞争锁;
  2. 非公平锁:线程请求锁时,先直接尝试获取锁(插队),只有获取失败时,才会加入等待队列尾部排队。

适用场景

  • 公平锁:对锁获取顺序有严格要求、不允许饥饿的场景(如金融交易、任务调度),牺牲性能换公平性;
  • 非公平锁:绝大多数业务场景(如普通接口同步、数据缓存更新),优先保证吞吐量和性能,容忍轻微的插队。
import java.util.concurrent.locks.ReentrantLock;

public class FairUnfairLockDemo {
    // 公平锁:传入true
    private static final ReentrantLock FAIR_LOCK = new ReentrantLock(true);
    // 非公平锁:传入false(或无参构造,默认非公平)
    private static final ReentrantLock UNFAIR_LOCK = new ReentrantLock(false);

    public static void main(String[] args) {
        // 测试公平锁
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                for (int j = 0; j < 2; j++) {
                    FAIR_LOCK.lock();
                    try {
                        System.out.println(Thread.currentThread().getName() + " 获取公平锁");
                    } finally {
                        FAIR_LOCK.unlock();
                    }
                }
            }, "公平线程-" + i).start();
        }

        // 测试非公平锁
        /*for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                for (int j = 0; j < 2; j++) {
                    UNFAIR_LOCK.lock();
                    try {
                        System.out.println(Thread.currentThread().getName() + " 获取非公平锁");
                    } finally {
                        UNFAIR_LOCK.unlock();
                    }
                }
            }, "非公平线程-" + i).start();
        }*/
    }
}

公平线程-0 获取公平锁
公平线程-1 获取公平锁
公平线程-3 获取公平锁
公平线程-2 获取公平锁
公平线程-4 获取公平锁
公平线程-0 获取公平锁
公平线程-1 获取公平锁
公平线程-3 获取公平锁
公平线程-2 获取公平锁
公平线程-4 获取公平锁


非公平线程-0 获取非公平锁
非公平线程-0 获取非公平锁
非公平线程-2 获取非公平锁
非公平线程-2 获取非公平锁
非公平线程-1 获取非公平锁
非公平线程-1 获取非公平锁
非公平线程-4 获取非公平锁
非公平线程-4 获取非公平锁
非公平线程-3 获取非公平锁
非公平线程-3 获取非公平锁

Java 公平锁与非公平锁 源码分析与核心流程(基于 ReentrantLock + AQS)

Java 中公平锁与非公平锁的核心实现依托于 java.util.concurrent.locks.ReentrantLock,其底层基于AQS(抽象队列同步器,AbstractQueuedSynchronizer) 实现,二者的核心差异体现在锁的获取逻辑上,释放逻辑完全一致。

一、核心底层依赖

  1. ReentrantLock:对外暴露锁的操作接口(lock()/unlock()),通过构造方法指定公平性(true= 公平锁,false= 非公平锁,无参默认非公平),内部持有一个同步器实现类 Sync(AQS 的子类);
  2. Sync 抽象类:ReentrantLock 的内部类,继承 AQS,实现锁的可重入性和基础释放逻辑,定义抽象方法 lock() 由子类实现(区分公平 / 非公平);
  3. NonfairSync/ FairSync:Sync 的两个子类,分别实现非公平锁公平锁的核心获取逻辑;
  4. AQS 核心属性:
    • state:volatile 修饰的 int 变量,代表锁的状态(0 = 无锁,>0 = 有线程持有锁,数值 = 重入次数);
    • exclusiveOwnerThread:持有排他锁的当前线程(AQS 的父类 AbstractOwnableSynchronizer 中的属性);
    • 双向等待队列:由 Node 节点组成,存放获取锁失败的线程,实现 FIFO 排队。

二、非公平锁(NonfairSync):源码 + 核心流程

非公平锁是 ReentrantLock 的默认实现,核心特点:线程请求锁时,先直接尝试抢占锁(插队),抢占失败后再加入 AQS 等待队列排队,这是其性能高于公平锁的关键。

(1)ReentrantLock 非公平锁初始化

// ReentrantLock 无参构造(默认非公平)
public ReentrantLock() {
    sync = new NonfairSync(); // 初始化非公平同步器
}
// 有参构造(指定false为非公平)
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

(2)NonfairSync 核心实现(ReentrantLock 内部类)

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    // 非公平锁的核心获取方法:ReentrantLock.lock() 最终调用此方法
    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);
    }
}

(3)Sync 中的 nonfairTryAcquire 方法(非公平锁尝试获取核心,可重入)

abstract static class Sync extends AbstractQueuedSynchronizer {
    // 非公平锁的尝试获取逻辑(公平锁不会调用此方法)
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState(); // 获取当前锁状态
        // 情况1:锁处于无锁状态(state=0)
        if (c == 0) {
            // 再次 CAS 抢占(无需检查队列,直接插队)
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 情况2:锁已被当前线程持有(可重入逻辑)
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires; // 重入次数+1
            if (nextc < 0) // 溢出保护(int 最大值+1会变为负数)
                throw new Error("Maximum lock count exceeded");
            setState(nextc); // 更新 state,无需 CAS(当前线程已持有锁,独占)
            return true;
        }
        // 情况3:锁被其他线程持有,获取失败
        return false;
    }
}

(4)AQS 中的 acquire 方法(公共框架,失败后入队)

public final void acquire(int arg) {
    // 核心逻辑:tryAcquire 失败 → 调用 addWaiter 将线程加入等待队列 → acquireQueued 阻塞等待
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt(); // 中断自我补充(防止阻塞期间中断信号丢失)
}

三、非公平锁完整获取流程(lock () 调用后)

核心口诀:先抢占 → 再重入 → 失败入队,共分 6 步:

  1. 线程调用 ReentrantLock.lock(),底层执行 NonfairSync.lock()

  2. 第一次 CAS 抢占:尝试将 AQS 的 state 从 0 改为 1,若成功,设置当前线程为锁持有者,获取锁完成;

  3. 若 CAS 失败,调用 AQS 的 acquire(1) 进入后续逻辑;

  4. acquire 调用 NonfairSync.tryAcquire(1),底层执行 nonfairTryAcquire(1)

  5. nonfairTryAcquire 做二次判断:

    • state=0(前一步抢占的线程已释放锁),再次 CAS 插队抢占,成功则获取锁;

    • 若当前线程已是锁持有者,重入次数 + 1,更新 state,获取锁完成;

    • 若锁被其他线程持有,返回 false,尝试获取失败;

  6. 尝试获取失败后,通过 addWaiter 将当前线程封装为排他型 Node 节点,加入 AQS 双向等待队列的尾部,再通过 acquireQueued 使线程进入阻塞状态,等待队列头部节点释放锁后被唤醒。

四、公平锁(FairSync):源码 + 核心流程

公平锁的核心特点:严格遵循 FIFO 先进先出,线程请求锁时,不会直接抢占,而是先检查 AQS 等待队列是否有排队的线程(队列非空则必须排队),只有队列无等待线程时,才尝试获取锁,从根本上避免 “插队”,保证公平性。

(1)FairSync 核心实现(ReentrantLock 内部类)

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    // 公平锁的核心获取方法:ReentrantLock.lock() 最终调用此方法
    final void lock() {
        // 关键差异1:直接调用 AQS 的 acquire(1),无前置 CAS 抢占逻辑
        acquire(1);
    }

    // 重写 AQS 的 tryAcquire 方法:公平锁的尝试获取逻辑(含可重入+队列检查)
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        // 情况1:锁处于无锁状态(state=0)
        if (c == 0) {
            // 关键差异2:先检查队列 → hasQueuedPredecessors(),再尝试 CAS
            // hasQueuedPredecessors():判断队列是否有“等待更久的线程”(队列非空且当前线程不是队列头节点的下一个节点)
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 情况2:锁已被当前线程持有(可重入逻辑,与非公平锁完全一致)
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        // 情况3:队列有等待线程 或 CAS 失败,获取失败
        return false;
    }
}

(2)AQS 中的 hasQueuedPredecessors 方法(公平锁核心,队列检查)

public final boolean hasQueuedPredecessors() {
    Node t = tail; // 队列尾节点
    Node h = head; // 队列头节点
    Node s;
    // 核心逻辑:队列非空(h != t)且 头节点的下一个节点(s)存在 且 s 不是当前线程 → 返回 true(有等待线程)
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

说明:AQS 队列的头节点是 “哨兵节点”(无实际线程,代表已释放锁的节点),真正的等待线程从 h.next 开始,因此只需检查 h.next 即可判断是否有等待更久的线程。

五、公平锁完整获取流程(lock () 调用后)

核心口诀:先查队列 → 再获取 → 失败入队,共分 5 步(与非公平锁的核心差异在第 2、3 步):

  1. 线程调用 ReentrantLock.lock(),底层执行 FairSync.lock()

  2. 直接调用 AQS 的 acquire (1),无前置 CAS 抢占逻辑(杜绝插队);

  3. acquire 调用 FairSync.tryAcquire(1),做核心判断:

    • state=0(无锁),先调用 hasQueuedPredecessors() 检查队列:若队列无等待线程,才尝试 CAS 获取锁,成功则设置锁持有者;若队列有等待线程,直接返回 false

    • 若当前线程已是锁持有者,重入次数 + 1,更新 state,获取锁完成;

    • 若锁被其他线程持有,返回 false,尝试获取失败;

  4. 尝试获取失败后,与非公平锁执行完全相同的逻辑:通过 addWaiter 将线程封装为排他型 Node 节点,加入 AQS 队列尾部;

  5. 最后通过 acquireQueued 使线程进入阻塞状态,等待被队列头部节点唤醒,严格按 FIFO 顺序获取锁。

五、公平锁与非公平锁的释放流程(完全一致)

二者的锁释放逻辑无任何差异,均由 Sync 类实现,依托 AQS 的 release 方法,核心保证可重入锁的正确释放(重入次数归 0 才真正释放锁)和唤醒队列下一个等待线程

(1)ReentrantLock.unlock () 对外接口

public void unlock() {
    sync.release(1); // 调用 AQS 的 release 方法
}

(2)AQS 中的 release 方法(公共框架)

public final boolean release(int arg) {
    // 第一步:尝试释放锁 → tryRelease 返回 true 表示锁已完全释放(重入次数归0)
    if (tryRelease(arg)) {
        Node h = head; // 获取队列头节点
        // 头节点不为空 且 节点状态正常 → 唤醒队列下一个等待线程
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h); // 唤醒后继节点(LockSupport.unpark())
        return true;
    }
    return false;
}

(3)Sync 中的 tryRelease 方法(实际释放逻辑,可重入)

protected final boolean tryRelease(int releases) {
    int c = getState() - releases; // 重入次数-1
    // 检查:只有锁持有者才能释放锁(否则抛异常,防止非法释放)
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 核心:重入次数归0 → 真正释放锁(清空持有者,state置0)
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c); // 更新state(若有重入,仅减少次数,不释放锁)
    return free; // 只有完全释放时才返回true
}
  1. 线程调用 ReentrantLock.unlock(),底层执行 sync.release(1)
  2. 调用 tryRelease(1),将 state 减 1,判断当前线程是否为锁持有者(非持有者抛异常);
  3. state 减至 0 → 清空锁持有者,标记为 “完全释放”,返回 true;若 state > 0(还有重入),仅更新 state,返回 false
  4. 若完全释放锁,获取 AQS 队列头节点,若头节点有效,调用 unparkSuccessor 唤醒头节点的下一个节点(队列中第一个等待线程);
  5. 被唤醒的线程会重新尝试获取锁(公平锁按队列顺序,非公平锁唤醒后仍会尝试插队)。

核心差异一句话总结

非公平锁在两个节点做了 “插队尝试”:① lock() 方法开头的直接 CAS 抢占;② tryAcquirestate=0 时的再次 CAS 抢占(无需检查队列);而公平锁全程无插队,必须先查队列再获取。

六、核心流程总结

1. 非公平锁获取核心

lock() → 直接 CAS 抢占 → 失败则 acquire(1)tryAcquire 再次 CAS 插队(无队列检查)→ 重入则更新次数 → 失败则入队阻塞。

2. 公平锁获取核心

lock() → 直接 acquire(1)tryAcquire 先查队列(hasQueuedPredecessors())→ 队列空则 CAS 获取 → 重入则更新次数 → 失败则入队阻塞。

3. 通用释放核心

unlock()release(1)tryRelease 减少重入次数 → 次数归 0 则清空持有者、state 置 0 → 唤醒队列下一个等待线程。

Logo

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

更多推荐