【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) 可开启公平模式。

相关推荐
猿究院_夏燊38 分钟前
MySQL 的四种隔离级别
java·数据库·mysql
码流子1 小时前
移动收费怎么做?手持终端+App+收费中心的架构实践
java·开发语言·架构
霸道流氓气质1 小时前
AI 流式对话与实时聊天平台:SSE 解码竞态、有状态会话隔离与背压取消的 Vue3 + Spring AI 生产级实战
java·人工智能·spring
云创智城-yuncitys2 小时前
YunParking 4.0:JDK 17 与 Spring Boot 3.2 的停车云升级
java·开发语言·spring boot
文丑颜不良啊2 小时前
LeetCode1661~1680题目解析
java·leetcode
子非鱼@Itfuture2 小时前
关于前后端联调中登录状态/登录信息失效?
java·前端
郭小李2 小时前
openFeign入门
java·开发语言·前端
wljt2 小时前
公司二-面试题
java·开发语言·mybatis
AI备案指南-满满2 小时前
算法备案审核不通过,一般是什么原因?
java·人工智能·算法·大模型备案·算法备案·大模型登记
秋天的怀念_David2 小时前
算法一千零一夜 · 第二十三夜 电力民生写成的曲线
java·程序人生·算法