Java 公平锁与非公平锁详解
一、基本概念
1. 公平锁 (Fair Lock)
定义 :多个线程按照申请锁的时间顺序获取锁,即"先来先得"原则
特点:
- 线程按请求锁的时间先后顺序排队
- 新线程到来时,会检查等待队列
- 如果队列中有等待线程,则新线程会加入队列尾部
- 保障资源分配的绝对公平性
2. 非公平锁 (Non-fair Lock)
定义:允许线程"插队",新线程可直接尝试获取锁,而不一定遵循请求顺序
特点:
- 新线程可以尝试"插队"直接获取锁
- 若获取失败才进入等待队列
- 锁释放时,可能唤醒队列中线程,也可能被新来的线程抢占
- 吞吐量通常比公平锁更高
二、工作流程对比
公平锁工作流程:
锁空闲? 空闲 队列空 队列非空 线程A请求锁 锁状态 检查等待队列 线程A获取锁 线程A进入等待队列 线程A执行 线程B唤醒获取锁
非公平锁工作流程:
直接尝试获取 是 否 锁释放时 成功 失败 线程A请求锁 获取成功? 线程A持有锁执行 进入等待队列 尝试与队列外线程竞争
三、Java中的实现与使用
1. ReentrantLock的公平/非公平控制
java
// 创建非公平锁(默认)
ReentrantLock nonFairLock = new ReentrantLock();
// 创建公平锁
ReentrantLock fairLock = new ReentrantLock(true);
// 使用示例
public void accessResource() {
fairLock.lock(); // 公平获取锁
try {
// 临界区代码
} finally {
fairLock.unlock(); // 确保释放锁
}
}
2. synchronized 与公平性
synchronized
关键字实现的是非公平锁- 开发者无法控制synchronized的公平性
- 需要公平锁时必须使用ReentrantLock
四、性能对比与适用场景
特性 | 公平锁 | 非公平锁 |
---|---|---|
吞吐量 | 较低 | 较高 |
线程等待时间 | 相对平均 | 差异较大(可能饥饿) |
实现复杂度 | 较高 | 相对简单 |
上下文切换 | 较多 | 较少 |
适用场景 | 1. 要求严格公平 2. 线程等待时间敏感 | 1. 高并发场景 2. 吞吐量优先 |
适用场景分析:
推荐公平锁的场景:
- 要求严格顺序执行的业务(如交易处理系统)
- 线程等待时间至关重要的情况
- 避免线程饥饿(如资源分配系统)
推荐非公平锁的场景:
- 高并发应用(最大吞吐量优先)
- 锁持有时间短且操作频繁
- 减少线程切换开销的场景
五、底层实现原理
1. 公平锁实现机制
java
// ReentrantLock的公平获取锁逻辑
protected final boolean tryAcquire(int acquires) {
// 检查是否有前驱节点在等待
if (getFirstQueuedThread() != Thread.currentThread() &&
hasQueuedPredecessors()) {
return false; // 队列中有等待线程,排队
}
// ...其他逻辑
}
2. 非公平锁实现机制
java
// 非公平获取锁的典型逻辑
final void lock() {
// 首先尝试直接获取锁(插队)
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 失败后再进入队列
}
3. CLH队列(锁的等待队列)
两种锁类型都使用CLH变体的FIFO等待队列:
- 线程加入队列时自旋检查前驱节点状态
- 当前驱节点释放锁时通知后续节点
六、实战示例
公平锁实例:
java
public class FairLockExample {
private final ReentrantLock lock = new ReentrantLock(true);
public void performTask() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获取锁");
// 模拟操作
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
// 测试:多个线程将按启动顺序获取锁
}
非公平锁实例:
java
public class NonFairLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void performTask() {
// 尝试插队获取锁
if (lock.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " 插队成功");
Thread.sleep(50);
} finally {
lock.unlock();
}
} else {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 排队获取");
Thread.sleep(100);
} finally {
lock.unlock();
}
}
}
}
七、如何选择合适的锁
决策因素:
- 吞吐量需求:高并发场景优先非公平锁
- 公平性要求:交易系统优先公平锁
- 锁持有时间 :
- 短时间操作:非公平锁
- 长时间操作:公平锁(减少饥饿)
- 系统资源 :
- CPU资源有限:非公平锁(减少切换)
- 内存资源充足:公平锁
默认建议 :大多数场景下非公平锁是更好的选择,因为:
- Java中
synchronized
默认就是非公平- ReentrantLock默认实现也是非公平
- 提供更好的整体吞吐量
八、总结
对比维度 | 公平锁 | 非公平锁 |
---|---|---|
排队机制 | 严格遵守FIFO | 允许插队 |
吞吐量 | 较低 | 较高 |
响应时间 | 可预测 | 不确定 |
实现难度 | 较复杂 | 较简单 |
避免饥饿 | 能 | 不能 |
核心结论:
- 公平锁保障绝对的请求顺序
- 非公平锁提供更高的吞吐量
- Java中锁的默认行为是非公平的(符合大多数场景)
- 在需要严格顺序的业务中才使用公平锁