Skip to content

公平锁 vs 非公平锁

公平有代价——选择之前先理解。

一个故事

想象排队买奶茶:

  • 公平锁:严格按排队的先后顺序,每个人都能喝到
  • 非公平锁:允许插队,效率高,但可能有人永远喝不到

默认情况

synchronized 是非公平锁。

ReentrantLock 默认也是非公平锁,但可以手动指定:

java
// 非公平锁(默认)
Lock unfairLock = new ReentrantLock();

// 公平锁
Lock fairLock = new ReentrantLock(true);

排队模型

非公平锁:可能插队

等待队列:  [T1] → [T2] → [T3] → [T4]

新线程 T5 来了,可能直接抢到锁(如果锁刚好释放)

公平锁:严格排队

等待队列:  [T1] → [T2] → [T3] → [T4]
新线程 T5 必须排在最后,等 T1-T4 都走完

性能对比

java
public class PerformanceDemo {

    private static final int THREADS = 10;
    private static final int OPS = 100_000;

    public static void main(String[] args) throws Exception {
        // 测试非公平锁
        System.out.println("非公平锁:");
        testLock(false);

        Thread.sleep(2000);

        // 测试公平锁
        System.out.println("公平锁:");
        testLock(true);
    }

    private static void testLock(boolean fair) throws Exception {
        ReentrantLock lock = new ReentrantLock(fair);
        long start = System.nanoTime();

        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < THREADS; i++) {
            threads.add(new Thread(() -> {
                for (int j = 0; j < OPS; j++) {
                    lock.lock();
                    try {
                        // 模拟临界区
                    } finally {
                        lock.unlock();
                    }
                }
            }));
        }

        threads.forEach(Thread::start);
        for (Thread t : threads) t.join();

        long duration = System.nanoTime() - start;
        System.out.println("总耗时: " + duration / 1_000_000 + " ms");
    }
}

典型结果

非公平锁: 150 ms
公平锁:   220 ms

公平锁慢 30-50%

为什么?因为公平锁需要维护等待队列,线程切换更频繁。

饥饿问题

非公平锁可能导致某些线程长期获取不到锁。

java
public class StarvationDemo {

    private static final ReentrantLock lock = new ReentrantLock(false); // 非公平

    public static void main(String[] args) {
        // 高优先级线程持续获取锁
        Thread greedy = new Thread(() -> {
            while (true) {
                lock.lock();
                try {
                    // 持有锁时间很短,但频繁获取
                    Thread.sleep(1);  // 只睡 1ms
                } catch (InterruptedException e) {
                    break;
                } finally {
                    lock.unlock();
                }
            }
        }, "Greedy");
        greedy.setPriority(Thread.MAX_PRIORITY);

        // 普通线程
        Thread normal = new Thread(() -> {
            System.out.println("普通线程: 等待获取锁...");
            lock.lock();
            try {
                System.out.println("普通线程: 终于拿到锁了!");
            } finally {
                lock.unlock();
            }
        }, "Normal");
        normal.setPriority(Thread.NORM_PRIORITY);

        greedy.start();
        normal.start();

        // 3 秒后强制结束
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {}
        System.exit(0);
    }
}

在竞争激烈时,普通线程可能很长时间都拿不到锁。

适用场景

用非公平锁的场景(默认)

java
// 高并发,性能优先
Lock lock = new ReentrantLock();

// 场景1:计数器
private final ReentrantLock counterLock = new ReentrantLock();

// 场景2:缓存访问
private final ReentrantLock cacheLock = new ReentrantLock();

// 场景3:快速操作
private final ReentrantLock quickLock = new ReentrantLock();

理由

  1. 性能好(减少线程切换)
  2. 大多数场景不需要严格公平
  3. 非公平锁在实践中「足够公平」

用公平锁的场景

java
// 需要严格按顺序
Lock fairLock = new ReentrantLock(true);

// 场景1:任务调度(必须按提交顺序执行)
// 场景2:FIFO 队列
// 场景3:资产生成(每个请求必须得到响应)

风险:公平锁在高并发下可能性能较差。

实战:公平锁的实际效果

java
public class FairnessTest {

    private static final ReentrantLock lock = new ReentrantLock(true); // 公平

    public static void main(String[] args) throws InterruptedException {
        AtomicInteger order = new AtomicInteger(0);
        int[] arrivalOrder = new int[5];
        int[] lockOrder = new int[5];
        CountDownLatch startLatch = new CountDownLatch(1);

        // 创建 5 个线程
        for (int i = 0; i < 5; i++) {
            final int id = i;
            arrivalOrder[i] = id;
            new Thread(() -> {
                try {
                    startLatch.await();  // 等待同时开始
                    lock.lock();
                    try {
                        lockOrder[id] = order.incrementAndGet();
                        System.out.println("线程 " + id + " 第 " + lockOrder[id] + " 个获取锁");
                    } finally {
                        lock.unlock();
                    }
                } catch (InterruptedException e) {}
            }, "Thread-" + i).start();
        }

        Thread.sleep(100);
        startLatch.countDown();  // 同时开始
        Thread.sleep(1000);

        System.out.println("\n到达顺序: " + Arrays.toString(arrivalOrder));
        System.out.println("获取锁顺序: " + Arrays.toString(lockOrder));
        System.out.println("公平锁保证了到达顺序 = 获取锁顺序");
    }
}

输出

线程 0 第 1 个获取锁
线程 1 第 2 个获取锁
线程 2 第 3 个获取锁
线程 3 第 4 个获取锁
线程 4 第 5 个获取锁

到达顺序: [0, 1, 2, 3, 4]
获取锁顺序: [1, 2, 3, 4, 5]

总结

对比项非公平锁公平锁
吞吐量
等待顺序不保证FIFO
饥饿风险
线程切换
适用场景性能优先顺序敏感

选择原则

  • 默认用非公平锁(性能好)
  • 需要严格顺序时用公平锁(如任务调度)
  • 高并发下慎用公平锁(可能性能暴跌)

记住:除非有明确的公平性需求,否则不要用公平锁。公平是有代价的。

基于 VitePress 构建