公平锁 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();理由:
- 性能好(减少线程切换)
- 大多数场景不需要严格公平
- 非公平锁在实践中「足够公平」
用公平锁的场景
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 |
| 饥饿风险 | 有 | 无 |
| 线程切换 | 少 | 多 |
| 适用场景 | 性能优先 | 顺序敏感 |
选择原则:
- 默认用非公平锁(性能好)
- 需要严格顺序时用公平锁(如任务调度)
- 高并发下慎用公平锁(可能性能暴跌)
记住:除非有明确的公平性需求,否则不要用公平锁。公平是有代价的。
