锁的公平性与竞争机制
公平锁与非公平锁概述
在多线程环境下,当多个线程竞争同一把锁时,如何决定哪个线程获得锁,这就涉及到锁的公平性问题。
mermaid
graph LR
A[锁的公平性] --> B[公平锁<br/>Fair Lock]
A --> C[非公平锁<br/>Nonfair Lock]
B --> D[按申请顺序<br/>FIFO队列]
C --> E[抢占式<br/>不保证顺序]
style B fill:#90EE90
style C fill:#FFB74D公平锁:先到先得
公平锁的定义
公平锁保证等待锁的线程按照请求锁的时间顺序获取锁,遵循先进先出(FIFO)原则。
java
import java.util.concurrent.locks.ReentrantLock;
public class FairLockDemo {
// 创建公平锁
private final ReentrantLock fairLock = new ReentrantLock(true);
public void accessResource() {
fairLock.lock();
try {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " 获得锁,时间: "
+ System.currentTimeMillis());
// 模拟业务处理
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
fairLock.unlock();
}
}
}公平锁的工作流程
mermaid
graph TD
A[线程A请求锁<br/>时间:T1] --> B[进入FIFO队列<br/>位置:1]
C[线程B请求锁<br/>时间:T2] --> D[进入FIFO队列<br/>位置:2]
E[线程C请求锁<br/>时间:T3] --> F[进入FIFO队列<br/>位置:3]
B --> G[锁释放]
G --> H[队首线程A获得锁]
H --> I[线程A释放锁]
I --> J[队首线程B获得锁]
J --> K[线程B释放锁]
K --> L[队首线程C获得锁]
style H fill:#90EE90
style J fill:#90EE90
style L fill:#90EE90公平锁的核心实现
ReentrantLock公平锁的核心源码(简化版):
java
static final class FairSync extends Sync {
final void lock() {
// 直接调用acquire,不会插队
acquire(1);
}
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;
}
}
else if (current == getExclusiveOwnerThread()) {
// 可重入
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 检查AQS队列中是否有等待线程
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
}关键点:
hasQueuedPredecessors()方法检查AQS队列中是否有等待线程- 只有队列为空或当前线程是队首时,才允许获取锁
- 保证了先到先得的公平性
公平锁的优缺点
mermaid
graph LR
A[公平锁] --> B[优点]
A --> C[缺点]
B --> D[所有线程<br/>都有机会]
B --> E[不会饥饿]
B --> F[顺序可预测]
C --> G[吞吐量低]
C --> H[线程切换<br/>开销大]
C --> I[性能较差]
style B fill:#90EE90
style C fill:#FF6B6B优点:
- 避免饥饿: 保证每个线程都能获得锁,不会有线程永远等待
- 顺序明确: 执行顺序可预测,便于调试和理解
- 公平性: 符合直觉,先到的先执行
缺点:
- 吞吐量下降: 维护FIFO队列需要额外开销
- 唤醒开销: 严格按顺序唤醒,增加了上下文切换
- 性能较差: 在高并发场景下性能明显低于非公平锁
公平锁的适用场景
公平锁适合以下场景:
- 业务有严格的顺序要求:
java
public class OrderProcessor {
private final ReentrantLock lock = new ReentrantLock(true);
// 按照订单提交顺序处理
public void processOrder(String orderId) {
lock.lock();
try {
System.out.println("处理订单: " + orderId);
// 订单处理逻辑
} finally {
lock.unlock();
}
}
}- 防止线程饥饿: 确保所有线程都有执行机会
- 需要可预测的执行顺序: 调试或测试场景
非公平锁:抢占式获取
非公平锁的定义
非公平锁不保证线程获取锁的顺序,允许新到达的线程"插队"直接竞争锁。
java
import java.util.concurrent.locks.ReentrantLock;
public class NonfairLockDemo {
// 创建非公平锁(默认)
private final ReentrantLock nonfairLock = new ReentrantLock();
public void accessResource() {
nonfairLock.lock();
try {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " 获得锁,时间: "
+ System.currentTimeMillis());
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
nonfairLock.unlock();
}
}
}非公平锁的工作流程
mermaid
graph TD
A[线程A请求锁<br/>时间:T1] --> B[进入等待队列]
C[线程B请求锁<br/>时间:T2] --> D[进入等待队列]
E[锁释放] --> F[线程C刚好到达<br/>时间:T3]
F --> G{直接尝试CAS获取锁}
G -->|成功| H[线程C获得锁<br/>插队成功!]
G -->|失败| I[进入等待队列]
B --> J[线程A等待中]
D --> K[线程B等待中]
style H fill:#FFB74D
style J fill:#E0E0E0
style K fill:#E0E0E0非公平锁的核心实现
ReentrantLock非公平锁的核心源码(简化版):
java
static final class NonfairSync extends Sync {
final void lock() {
// 关键:直接尝试CAS获取锁,不检查队列
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // CAS失败才进入队列
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 关键:不检查队列,直接CAS获取
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 可重入
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}关键点:
lock()方法首先直接CAS尝试获取锁,不检查队列- CAS成功则直接获得锁,实现"插队"
- CAS失败才进入队列等待
非公平锁的优缺点
mermaid
graph LR
A[非公平锁] --> B[优点]
A --> C[缺点]
B --> D[吞吐量高]
B --> E[性能好]
B --> F[减少线程切换]
C --> G[可能饥饿]
C --> H[顺序不确定]
C --> I[不公平]
style B fill:#90EE90
style C fill:#FF6B6B优点:
- 高吞吐量: 减少了线程切换和调度开销
- 性能优秀: 在高并发下表现更好
- 避免唤醒延迟: 新线程可能在等待线程被唤醒前获得锁
缺点:
- 可能饥饿: 某些线程可能长时间获取不到锁
- 不可预测: 无法预知线程的执行顺序
- 不公平: 不符合先来先服务的直觉
为什么非公平锁性能更好
mermaid
graph TB
A[锁释放] --> B{公平锁}
A --> C{非公平锁}
B --> D[必须唤醒队首线程]
D --> E[线程从BLOCKED到RUNNABLE]
E --> F[操作系统调度]
F --> G[线程获得CPU]
G --> H[执行加锁代码]
C --> I[新线程直接CAS]
I --> J[立即获得锁]
J --> K[立即执行]
H --> L[总耗时:较长]
K --> M[总耗时:很短]
style L fill:#FFB74D
style M fill:#90EE90性能差异的根本原因:
- 线程状态切换:
- 公平锁:BLOCKED → RUNNABLE → RUNNING(需要操作系统介入)
- 非公平锁:可能直接获取,无需状态切换
- 上下文切换开销:
- 公平锁:严格按队列唤醒,增加上下文切换
- 非公平锁:减少不必要的线程切换
- 缓存利用:
- 刚释放锁的线程,其CPU缓存可能还有效
- 如果新线程刚好到达,可以利用热缓存,提升性能
公平锁 vs 非公平锁对比
详细对比表
| 对比维度 | 公平锁 | 非公平锁 |
|---|---|---|
| 获取顺序 | 严格FIFO | 允许插队 |
| 实现复杂度 | 需维护队列顺序 | 实现相对简单 |
| 吞吐量 | 较低 | 较高 |
| 线程切换 | 频繁 | 较少 |
| 饥饿可能 | 不会饥饿 | 可能饥饿 |
| 适用场景 | 顺序要求严格 | 高并发高性能 |
| Java默认 | 非默认 | Synchronized、ReentrantLock默认 |
性能测试对比
java
public class LockPerformanceTest {
private static final int THREAD_COUNT = 100;
private static final int ITERATIONS = 10000;
// 公平锁测试
public static long testFairLock() {
ReentrantLock lock = new ReentrantLock(true);
long startTime = System.currentTimeMillis();
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
for (int j = 0; j < ITERATIONS; j++) {
lock.lock();
try {
// 模拟业务操作
} finally {
lock.unlock();
}
}
latch.countDown();
}).start();
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
return System.currentTimeMillis() - startTime;
}
// 非公平锁测试
public static long testNonfairLock() {
ReentrantLock lock = new ReentrantLock(false);
// ... 同样的测试逻辑
return 0;
}
}典型测试结果(实际结果因环境而异):
- 公平锁耗时: 约3500ms
- 非公平锁耗时: 约2000ms
- 性能差距: 非公平锁快约40-50%
Synchronized的公平性
Synchronized是非公平锁
前面章节提到,Synchronized是一种非公平锁:
java
public class SynchronizedFairnessTest {
private final Object lock = new Object();
public void accessResource() {
synchronized(lock) {
System.out.println(Thread.currentThread().getName()
+ " 获得锁");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
SynchronizedFairnessTest test = new SynchronizedFairnessTest();
// 启动10个线程
for (int i = 0; i < 10; i++) {
new Thread(() -> test.accessResource(),
"Thread-" + i).start();
}
}
}输出可能是:
plain
Thread-0 获得锁
Thread-5 获得锁 // 不按顺序
Thread-2 获得锁
Thread-7 获得锁
...Synchronized非公平的体现
mermaid
graph TD
A[线程1持有Synchronized锁] --> B[线程2、3、4在EntryList等待]
A --> C[线程1释放锁]
C --> D{JVM/OS调度}
D --> E[可能线程5刚到达<br/>直接获取锁]
D --> F[也可能唤醒线程2]
D --> G[也可能唤醒线程3]
style E fill:#FFB74D
style F fill:#FFB74D
style G fill:#FFB74DSynchronized的非公平性体现在:
- JVM层面不保证顺序: Monitor的EntryList不是严格的FIFO队列
- 操作系统调度的不确定性: 线程从BLOCKED到RUNNABLE的时间不确定
- 性能优先: 减少线程切换开销,提高吞吐量
如何实现公平的Synchronized
如果业务必须保证公平性,可以自己实现:
java
public class FairSynchronized {
private final Object lock = new Object();
private final Queue<Thread> waitQueue =
new LinkedBlockingQueue<>();
private volatile Thread currentThread = null;
public void fairLock() throws InterruptedException {
Thread thread = Thread.currentThread();
waitQueue.offer(thread);
synchronized(lock) {
while (waitQueue.peek() != thread ||
currentThread != null) {
lock.wait();
}
waitQueue.poll();
currentThread = thread;
}
}
public void fairUnlock() {
synchronized(lock) {
currentThread = null;
lock.notifyAll();
}
}
}但这种实现复杂且性能差,不如直接使用ReentrantLock的公平模式。
锁的选择建议
mermaid
graph TD
A[选择锁机制] --> B{是否需要公平性?}
B -->|是| C[使用ReentrantLock<br/>公平模式]
B -->|否| D{是否需要高级特性?}
D -->|否| E[使用Synchronized<br/>简单高效]
D -->|是| F{需要哪些特性?}
F --> G[超时、中断、条件变量]
G --> H[使用ReentrantLock<br/>非公平模式]
style C fill:#90EE90
style E fill:#4CAF50
style H fill:#64B5F6选择原则
- 默认选择: 优先使用Synchronized,简单且性能好
- 需要公平性: 使用
new ReentrantLock(true) - 需要超时/中断: 使用ReentrantLock的高级特性
- 读多写少: 考虑ReadWriteLock或StampedLock
- 性能关键: 非公平锁性能更好,优先选择
实际案例
java
public class LockSelectionExample {
// 场景1:简单计数器 -> Synchronized
private int counter = 0;
public synchronized void increment() {
counter++;
}
// 场景2:需要公平性 -> 公平ReentrantLock
private final ReentrantLock fairLock = new ReentrantLock(true);
public void processInOrder() {
fairLock.lock();
try {
// 按顺序处理
} finally {
fairLock.unlock();
}
}
// 场景3:需要超时 -> ReentrantLock
private final ReentrantLock timeoutLock = new ReentrantLock();
public boolean tryProcess(long timeout)
throws InterruptedException {
if (timeoutLock.tryLock(timeout, TimeUnit.SECONDS)) {
try {
// 处理逻辑
return true;
} finally {
timeoutLock.unlock();
}
}
return false;
}
}核心要点总结
- 公平锁: 按FIFO顺序获取锁,避免饥饿,但性能较差
- 非公平锁: 允许插队,性能好,但可能导致饥饿
- 性能差距: 非公平锁性能通常比公平锁高40-50%
- Synchronized: 是非公平锁,JVM和OS不保证获取顺序
- ReentrantLock: 可选公平或非公平模式,默认非公平
- 选择建议: 除非业务明确需要公平性,否则优先选择非公平锁
- 实现机制: 公平锁检查队列,非公平锁直接CAS尝试
更新: 2025-12-04 17:36:05
原文: https://www.yuque.com/u22210564/zoxfmt/doc-07-12-13