在 Java 多线程中,公平锁和非公平锁是ReentrantLock提供的两种锁获取策略,核心区别在于:多个线程竞争锁时,是否严格按照“先来后到”的顺序获取锁。
下面从原理、实现、优缺点和代码示例几个方面来讲。
一、基本概念
1. 公平锁(Fair Lock)
- 多个线程按照申请锁的顺序来获取锁,先到先得。
- 线程会进入一个 FIFO 队列排队,只有队首的线程才能获取锁。
- 新来的线程不能插队,即使锁刚好空闲,也要先看队列里有没有等待的线程。
2. 非公平锁(Nonfair Lock)
- 不保证顺序,允许插队。
- 新来的线程可以直接尝试 CAS 抢锁,如果抢到了就直接执行,不管队列里有没有等待的线程。
- 抢不到再进入队列等待。
二、源码层面的区别
ReentrantLock内部通过Sync抽象类实现,有两个子类:
publicclassReentrantLockimplementsLock{privatefinalSyncsync;abstractstaticclassSyncextendsAbstractQueuedSynchronizer{// ...}// 非公平锁staticfinalclassNonfairSyncextendsSync{...}// 公平锁staticfinalclassFairSyncextendsSync{...}}非公平锁的加锁逻辑(简化)
finalvoidlock(){// 直接 CAS 抢锁,不管有没有队列if(compareAndSetState(0,1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);// 抢不到再走正常流程}tryAcquire中也不会检查队列:
protectedfinalbooleantryAcquire(intacquires){returnnonfairTryAcquire(acquires);}公平锁的加锁逻辑
protectedfinalbooleantryAcquire(intacquires){finalThreadcurrent=Thread.currentThread();intc=getState();if(c==0){// 关键:先判断队列中是否有前驱节点if(!hasQueuedPredecessors()&&compareAndSetState(0,acquires)){setExclusiveOwnerThread(current);returntrue;}}// ... 可重入逻辑}hasQueuedPredecessors()是公平锁的核心:如果队列里已经有线程在等,就不抢,乖乖排队。
三、公平锁 vs 非公平锁对比
| 维度 | 公平锁 | 非公平锁 |
|---|---|---|
| 获取顺序 | 严格 FIFO | 允许插队 |
| 吞吐量 | 较低 | 较高 |
| 线程饥饿 | 不会 | 可能(理论上) |
| 上下文切换 | 较多 | 较少 |
| 实现复杂度 | 略高 | 略低 |
| 默认策略 | 否 | 是(ReentrantLock 默认非公平) |
为什么非公平锁吞吐量更高?
因为公平锁每次都要唤醒队首线程,涉及线程切换(用户态→内核态),开销大。而非公平锁允许刚释放锁的线程或新来的线程立刻抢到锁,减少了线程唤醒和切换的次数。
注意:非公平锁虽然可能“插队”,但一旦进入队列,仍然是 FIFO 的,只是新来的线程有机会在入队前抢到锁。
四、代码示例
importjava.util.concurrent.locks.ReentrantLock;publicclassFairLockDemo{// true 表示公平锁,false 表示非公平锁privatestaticfinalReentrantLockfairLock=newReentrantLock(true);privatestaticfinalReentrantLockunfairLock=newReentrantLock(false);publicstaticvoidmain(String[]args){testLock(fairLock,"公平锁");System.out.println("----------");testLock(unfairLock,"非公平锁");}privatestaticvoidtestLock(ReentrantLocklock,Stringname){Runnabletask=()->{for(inti=0;i<2;i++){lock.lock();try{System.out.println(name+" -> "+Thread.currentThread().getName()+" 获取锁");}finally{lock.unlock();}}};for(inti=0;i<5;i++){newThread(task,"线程-"+i).start();}try{Thread.sleep(1000);}catch(InterruptedExceptione){}}}公平锁下,输出顺序基本是线程-0、线程-1、线程-2...轮转;非公平锁下顺序可能混乱,某些线程会连续获取多次。
五、其他相关点
1. synchronized 是公平锁吗?
不是。synchronized是非公平的,且不支持设置为公平锁。它的锁获取策略由 JVM 实现,偏向锁、轻量级锁、重量级锁都偏向于性能,不保证公平。
2. 读写锁的公平性
ReentrantReadWriteLock也支持公平/非公平:
newReentrantReadWriteLock(true);// 公平3. 什么时候用公平锁?
- 业务要求严格按顺序执行,比如某些资源调度。
- 需要避免线程饥饿的场景。
- 大多数情况下,优先用非公平锁(默认),性能更好。
六、一句话总结
公平锁按排队顺序获取锁,避免饥饿但性能较低;非公平锁允许插队,吞吐量高但可能造成线程饥饿。
ReentrantLock默认是非公平锁,通过构造函数new ReentrantLock(true)可开启公平模式。