【JUC】公平锁与非公平锁

在 Java 多线程中,公平锁 和非公平锁 是 ReentrantLock 提供的两种锁获取策略,核心区别在于:多个线程竞争锁时,是否严格按照"先来后到"的顺序获取锁。

下面从原理、实现、优缺点和代码示例几个方面来讲。


一、基本概念

1. 公平锁(Fair Lock)

  • 多个线程按照申请锁的顺序来获取锁,先到先得。
  • 线程会进入一个 FIFO 队列排队,只有队首的线程才能获取锁。
  • 新来的线程不能插队,即使锁刚好空闲,也要先看队列里有没有等待的线程。

2. 非公平锁(Nonfair Lock)

  • 不保证顺序,允许插队。
  • 新来的线程可以直接尝试 CAS 抢锁,如果抢到了就直接执行,不管队列里有没有等待的线程。
  • 抢不到再进入队列等待。

二、源码层面的区别

ReentrantLock 内部通过 Sync 抽象类实现,有两个子类:

java 复制代码
public class ReentrantLock implements Lock {
    private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer {
        // ...
    }

    // 非公平锁
    static final class NonfairSync extends Sync { ... }

    // 公平锁
    static final class FairSync extends Sync { ... }
}

非公平锁的加锁逻辑(简化)

java 复制代码
final void lock() {
    // 直接 CAS 抢锁,不管有没有队列
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);  // 抢不到再走正常流程
}

tryAcquire 中也不会检查队列:

java 复制代码
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

公平锁的加锁逻辑

java 复制代码
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;
        }
    }
    // ... 可重入逻辑
}

hasQueuedPredecessors() 是公平锁的核心:如果队列里已经有线程在等,就不抢,乖乖排队。


三、公平锁 vs 非公平锁对比

维度 公平锁 非公平锁
获取顺序 严格 FIFO 允许插队
吞吐量 较低 较高
线程饥饿 不会 可能(理论上)
上下文切换 较多 较少
实现复杂度 略高 略低
默认策略 否 是(ReentrantLock 默认非公平)

为什么非公平锁吞吐量更高?

因为公平锁每次都要唤醒队首线程,涉及线程切换(用户态→内核态),开销大。而非公平锁允许刚释放锁的线程或新来的线程立刻抢到锁,减少了线程唤醒和切换的次数。

注意:非公平锁虽然可能"插队",但一旦进入队列,仍然是 FIFO 的,只是新来的线程有机会在入队前抢到锁。


四、代码示例

java 复制代码
import java.util.concurrent.locks.ReentrantLock;

public class FairLockDemo {
    // true 表示公平锁,false 表示非公平锁
    private static final ReentrantLock fairLock = new ReentrantLock(true);
    private static final ReentrantLock unfairLock = new ReentrantLock(false);

    public static void main(String[] args) {
        testLock(fairLock, "公平锁");
        System.out.println("----------");
        testLock(unfairLock, "非公平锁");
    }

    private static void testLock(ReentrantLock lock, String name) {
        Runnable task = () -> {
            for (int i = 0; i < 2; i++) {
                lock.lock();
                try {
                    System.out.println(name + " -> " + Thread.currentThread().getName() + " 获取锁");
                } finally {
                    lock.unlock();
                }
            }
        };

        for (int i = 0; i < 5; i++) {
            new Thread(task, "线程-" + i).start();
        }

        try { Thread.sleep(1000); } catch (InterruptedException e) { }
    }
}

公平锁下,输出顺序基本是 线程-0、线程-1、线程-2... 轮转;非公平锁下顺序可能混乱,某些线程会连续获取多次。


五、其他相关点

1. synchronized 是公平锁吗?

不是 。synchronized 是非公平的,且不支持设置为公平锁。它的锁获取策略由 JVM 实现,偏向锁、轻量级锁、重量级锁都偏向于性能,不保证公平。

2. 读写锁的公平性

ReentrantReadWriteLock 也支持公平/非公平:

java 复制代码
new ReentrantReadWriteLock(true);  // 公平

3. 什么时候用公平锁?

  • 业务要求严格按顺序执行,比如某些资源调度。
  • 需要避免线程饥饿的场景。
  • 大多数情况下,优先用非公平锁(默认),性能更好。

六、一句话总结

公平锁按排队顺序获取锁,避免饥饿但性能较低;非公平锁允许插队,吞吐量高但可能造成线程饥饿。ReentrantLock 默认是非公平锁,通过构造函数 new ReentrantLock(true) 可开启公平模式。

相关推荐
郑州光合科技余经理1 小时前
本地生活服务系统:预约状态和进度通知接口怎么拆
java·开发语言·前端·javascript·后端·php·生活
靠沿1 小时前
MySQL数据库基础——事务
java·数据库·mysql
Joy T7 小时前
Agent 确定性上下文设计:从 Prompt 参数注入到能力分层
java·开发语言·微服务·架构·prompt·agent context
m0_5873830010 小时前
广州 24 小时自助健身房解决方案技术实现与实战指南
java·spring·小程序·架构·需求分析
·云扬·10 小时前
Prompt 工程与安全:从 System 设计到注入防御
java·人工智能·安全·ai·prompt
代码不停11 小时前
自动化测试(java)
java·开发语言·selenium·自动化
鬼手点金13 小时前
opencode-rules 插件
java·开发语言·前端·人工智能·chrome·git·github
paopaokaka_luck14 小时前
滑雪场管理系统(AI智能助手流式对话,ECharts 运营数据分析、教练、雪具与场地一体化预约,钱包支付退款、订单核销与服务评价)
java·人工智能·spring boot·网络协议·数据分析·echarts
Wang's Blog14 小时前
Java 项目实战: 交友项目 - Dubbo 的内部结构与一次调用的完整链路
java·dubbo