ReentrantLock 详解
ReentrantLock 是 AQS 独占模式的经典实现,通过重写 tryAcquire、tryRelease、isHeldExclusively 等钩子方法,实现了可重入、公平性选择、可中断获取、超时等待、多条件变量、状态监控等丰富特性。其非公平锁策略通过减少上下文切换显著提升了吞吐量,而公平锁则保证了严格的 FIFO 顺序
一、核心特性与实现原理
ReentrantLock 的核心特性及其底层实现原理,均围绕 AQS(AbstractQueuedSynchronizer) 框架展开。每个特性背后都有明确的源码逻辑支撑。
| 特性 | 实现原理 | 关键源码片段 |
|---|---|---|
| 可重入性 | 通过 AQS 的 volatile int state 记录锁的重入次数。同一线程再次获取锁时 state++,释放时 state--,当 state == 0 时完全释放锁。 | Sync.nonfairTryAcquire: if (current == owner) { int nextc = c + acquires; setState(nextc); return true; } |
| 公平性选择 | 公平锁:在 tryAcquire 中先调用 hasQueuedPredecessors() 检查是否有线程在队列中等待更久,有则直接放弃获取。 非公平锁:lock() 时先 CAS 抢一次锁,失败后再进入 acquire 流程,tryAcquire 中再次 CAS 尝试,完全忽略队列。 | FairSync.tryAcquire: if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) |
| 可中断获取 | lockInterruptibly() 调用 AQS 的 acquireInterruptibly,在队列等待期间若检测到中断标记,立即抛出 InterruptedException。 | AQS.doAcquireInterruptibly: if (parkAndCheckInterrupt()) throw new InterruptedException(); |
| 超时获取 | tryLock(timeout, unit) 调用 tryAcquireNanos,使用 LockSupport.parkNanos 限时阻塞,超时返回 false。 | AQS.doAcquireNanos: nanosTimeout = deadline - System.nanoTime(); if (nanosTimeout <= 0L) return false; |
| 非阻塞尝试 | tryLock() 直接调用 nonfairTryAcquire(1),执行一次 CAS,不排队,立即返回结果。 | ReentrantLock.tryLock: return sync.nonfairTryAcquire(1); |
| 多条件变量 | 每个 Condition 对应 AQS 内部的 ConditionObject,维护独立的单向条件队列。await() 将线程从同步队列转移到条件队列,signal() 将线程从条件队列转移回同步队列尾部。 | ConditionObject.await: addConditionWaiter(); fullyRelease(node); park(); |
| 状态监控 | 直接读取 AQS 的 volatile 变量(如 state)或调用 AQS 提供的查询方法(如遍历 CLH 队列)。 | getHoldCount()、getQueueLength()、isHeldByCurrentThread() |
详细分析
1. 可重入性
原理:AQS 的 volatile int state 字段记录锁的重入次数。当锁被某线程持有时,state > 0。同一线程再次获取锁,state 累加;释放锁时 state 递减,只有当 state == 0 时才完全释放锁,并唤醒后继线程。
详细源码(Sync.nonfairTryAcquire) :
/**
* 非公平尝试获取锁的核心逻辑。
* 此方法被 NonfairSync.tryAcquire 和 tryLock() 调用。
* @param acquires 获取锁的计数,通常为 1
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 情况1:锁当前空闲(state == 0)
if (c == 0) {
// 直接 CAS 尝试抢占,不检查队列
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current); // 设置当前线程为独占所有者
return true;
}
}
// 情况2:锁已被持有,但持有者正是当前线程 —— 可重入的核心逻辑
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires; // 累加重入次数
if (nextc < 0) // 溢出检查(int 最大值)
throw new Error("Maximum lock count exceeded");
setState(nextc); // 更新 state,无需 CAS(因为已经独占)
return true;
}
// 情况3:锁被其他线程持有,获取失败
return false;
}
释放锁源码(Sync.tryRelease) :
/**
* 尝试释放锁。
* 此方法是 AQS 的模板方法,公平锁和非公平锁共用。
* @param releases 释放的计数,通常为 1
* @return true 表示锁已完全释放,AQS 会继续唤醒后继节点
*/
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 安全检查:只有锁的持有者才能释放
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
// state 归零,锁完全释放
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
2. 公平性选择
原理:
- 非公平锁
NonfairSync:在lock()时先进行一次 CAS 抢锁(插队),失败后再进入 AQS 的标准acquire流程,在tryAcquire中再次 CAS 尝试,完全不检查等待队列。 - 公平锁
FairSync:lock()直接调用acquire(1),在tryAcquire中必须先调用hasQueuedPredecessors()检查是否有线程在队列中等待时间更长,有则立即放弃获取。
非公平锁完整源码:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* 非公平锁的 lock 方法:先尝试一次插队 CAS
*/
final void lock() {
// 第一次插队尝试:如果锁空闲,直接抢
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 抢失败,进入 AQS 标准获取流程(内部会再次调用 tryAcquire)
acquire(1);
}
/**
* 非公平锁的 tryAcquire 直接复用 Sync 中的 nonfairTryAcquire
*/
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
公平锁完整源码:
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
/**
* 公平锁的 lock 方法:直接走 AQS 标准流程,不尝试插队
*/
final void lock() {
acquire(1);
}
/**
* 公平锁的 tryAcquire:必须检查队列中是否有前驱等待者
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 关键判断:hasQueuedPredecessors() 检查队列中是否有等待更久的线程
// 只有队列为空或当前线程是第一个等待者时,才允许 CAS 抢锁
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;
}
}
hasQueuedPredecessors() 源码(AQS 中) :
/**
* 判断当前线程前面是否有等待的线程。
* 返回 true 表示有前驱节点,当前线程必须排队。
*/
public final boolean hasQueuedPredecessors() {
Node t = tail; // 队尾
Node h = head; // 队头
Node s;
// 1. h != t 说明队列不为空
// 2. 队列不为空时,检查第一个有效等待节点(head.next)的线程是否不是当前线程
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
3. 可中断获取
原理:lockInterruptibly() 调用 AQS 的 acquireInterruptibly(1),在等待队列中自旋挂起时,若检测到线程的中断标志为 true,立即抛出 InterruptedException,而不是继续等待。
源码链路:
// ReentrantLock 中的入口
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
// AQS.acquireInterruptibly
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted()) // 响应中断的快速检查
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg); // 核心等待逻辑
}
// AQS.doAcquireInterruptibly(关键片段)
private void doAcquireInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
return;
}
// 挂起前检查是否需要挂起,若需要则执行 parkAndCheckInterrupt
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 被唤醒后,如果是被中断唤醒的,直接抛出异常
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
// parkAndCheckInterrupt 方法
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 挂起当前线程
return Thread.interrupted(); // 返回中断状态并清除中断标志
}
4. 超时获取
原理:tryLock(long timeout, TimeUnit unit) 最终调用 AQS 的 doAcquireNanos,在自旋过程中计算剩余超时时间,使用 LockSupport.parkNanos 进行限时阻塞。超时则返回 false,取消排队。
源码链路:
// ReentrantLock 入口
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
// AQS.tryAcquireNanos
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
}
// AQS.doAcquireNanos(关键片段)
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout; // 计算截止时间
final Node node = addWaiter(Node.EXCLUSIVE);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
return true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L) {
// 超时,取消获取并返回 false
cancelAcquire(node);
return false;
}
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
// 限时阻塞指定的纳秒数
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
5. 非阻塞尝试
原理:tryLock() 直接调用 nonfairTryAcquire(1),仅执行一次 CAS 尝试,不涉及 AQS 队列操作,成功即返回 true,失败立即返回 false。
源码:
public boolean tryLock() {
return sync.nonfairTryAcquire(1); // 单次尝试,非公平逻辑
}
6. 多条件变量
原理:newCondition() 返回 AQS 内部类 ConditionObject 的新实例。每个 ConditionObject 内部维护一个单向条件队列(由 Node 的 nextWaiter 字段链接)。await() 会将当前线程的节点从 CLH 同步队列转移到条件队列,并完全释放锁;signal() 会将条件队列的头节点转移回 CLH 同步队列尾部,等待被唤醒后重新竞争锁。
newCondition() 源码:
public Condition newCondition() {
return sync.newCondition();
}
// Sync 中的方法
final ConditionObject newCondition() {
return new ConditionObject(); // ConditionObject 是 AQS 的非静态内部类
}
await() 核心逻辑(AQS.ConditionObject) :
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 1. 将当前线程包装成节点加入条件队列(状态为 CONDITION)
Node node = addConditionWaiter();
// 2. 完全释放当前线程持有的锁(state 置 0,并唤醒后继)
int savedState = fullyRelease(node);
int interruptMode = 0;
// 3. 循环检查节点是否还在条件队列中(若不在说明已被 signal 转移到同步队列)
while (!isOnSyncQueue(node)) {
LockSupport.park(this); // 挂起等待 signal
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 4. 被 signal 唤醒后,重新竞争锁(调用 acquireQueued)
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
signal() 核心逻辑:
public final void signal() {
if (!isHeldExclusively()) // 必须由锁持有者调用
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first); // 将第一个等待节点从条件队列转移到同步队列
}
private void doSignal(Node first) {
do {
if ((firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) && // 转移节点
(first = firstWaiter) != null);
}
// 将节点从条件队列转移到同步队列
final boolean transferForSignal(Node node) {
// 修改节点状态为 0(初始状态)
if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
return false;
// 加入同步队列尾部,并返回前驱节点
Node p = enq(node);
int ws = p.waitStatus;
// 如果前驱节点已取消或 CAS 设置 SIGNAL 失败,直接唤醒当前线程
if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
7. 状态监控
原理:ReentrantLock 提供了多个查询方法,这些方法直接读取 AQS 的 volatile 变量(如 state、exclusiveOwnerThread)或调用 AQS 提供的遍历方法。
源码示例:
// 获取当前线程持有该锁的重入次数
public int getHoldCount() {
return sync.getHoldCount();
}
// Sync 中的实现
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
// 判断锁是否被当前线程持有
public boolean isHeldByCurrentThread() {
return sync.isHeldExclusively();
}
// Sync 中的实现
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
// 判断锁是否被任意线程持有
public boolean isLocked() {
return sync.isLocked();
}
// Sync 中的实现
final boolean isLocked() {
return getState() != 0;
}
// 获取等待获取锁的线程数估计值
public final int getQueueLength() {
return sync.getQueueLength();
}
// AQS 中的实现(遍历 CLH 队列)
public final int getQueueLength() {
int n = 0;
for (Node p = tail; p != null; p = p.prev) {
if (p.thread != null)
++n;
}
return n;
}
二、与 AQS 之间的关系
ReentrantLock 内部定义了一个继承自 AbstractQueuedSynchronizer 的抽象类 Sync,并派生出 NonfairSync 和 FairSync 两个子类。AQS 采用了模板方法设计模式,Sync 仅需重写以下几个关键钩子方法,而线程的阻塞、排队、唤醒等复杂操作均由 AQS 父类完成。
Sync 抽象类结构:
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
// 1. 留给子类实现的抽象方法(公平/非公平的核心差异在此)
abstract void lock();
// 2. 重写 AQS 的 tryRelease(公平与非公平释放逻辑一致)
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
// 3. 重写 AQS 的 isHeldExclusively(用于 Condition 判断)
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
// 4. 提供通用的非公平尝试获取逻辑(供 NonfairSync 和 tryLock 调用)
final boolean nonfairTryAcquire(int acquires) {
// 源码见第一部分
}
// 5. 提供 Condition 实例创建方法
final ConditionObject newCondition() {
return new ConditionObject();
}
// 6. 提供状态监控的辅助方法
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
final boolean isLocked() {
return getState() != 0;
}
// ... 其他监控方法 ...
}
AQS 钩子方法重写一览表:
| AQS 钩子方法 | 在 ReentrantLock 中的重写位置 | 作用描述 |
|---|---|---|
tryAcquire(int acquires) | NonfairSync / FairSync | 独占模式获取锁。尝试修改 state,成功返回 true。 |
tryRelease(int releases) | Sync 中统一实现 | 独占模式释放锁。减少 state,减至 0 时释放所有者。 |
tryAcquireNanos(...) | 未重写 | 直接使用 AQS 默认实现,内部调用 tryAcquire。 |
tryAcquireInterruptibly(...) | 未重写 | 同上。 |
isHeldExclusively() | Sync 中实现 | 判断当前线程是否独占持有锁。 |
三、加锁与解锁流程
以下时序图详细对比了公平锁与非公平锁在竞争失败入队及释放唤醒时的完整交互流程。
3.1 竞争失败入队流程(线程B尝试获取已被线程A持有的锁)
sequenceDiagram
participant B as 线程B (新来者)
participant Lock as ReentrantLock
participant Sync as 同步器 (Fair/Nonfair)
participant State as AQS State
participant Queue as AQS 同步队列 (CLH)
B->>Lock: lock()
activate Lock
Lock->>Sync: 委托给内部 Sync 执行
rect rgb(255, 245, 235)
Note over Sync,State: === 非公平锁路径 (NonfairSync) ===
Sync->>State: 第一次 CAS 尝试将 0 改为 1 (插队)
State-->>Sync: 失败 (当前 state != 0)
Sync->>Sync: acquire(1) -> tryAcquire(1)
Sync->>State: 第二次 CAS 尝试 (若 state=0 则抢)
State-->>Sync: 再次失败
end
rect rgb(235, 245, 255)
Note over Sync,State: === 公平锁路径 (FairSync) ===
Sync->>Sync: acquire(1) -> tryAcquire(1)
Sync->>Sync: hasQueuedPredecessors()?
Note over Sync: 检查队列中是否有等待者
alt 队列不为空 (有前驱节点)
Sync-->>B: 返回 false,放弃尝试
end
end
Note over Sync,Queue: 双方殊途同归:进入队列等待
Sync->>Queue: addWaiter(Node.EXCLUSIVE)
Note over Queue: CAS 将线程B包装成节点插入队尾
Queue-->>Sync: 返回节点
Sync->>B: acquireQueued() -> parkAndCheckInterrupt()
deactivate Lock
Note over B: 线程B挂起 (LockSupport.park),等待唤醒
3.2 释放锁与唤醒后继流程(线程A释放,新线程C同时到达)
sequenceDiagram
participant A as 线程A (持锁者)
participant Lock as ReentrantLock
participant State as AQS State
participant Queue as AQS 同步队列
participant C as 新线程C (插队者)
participant B as 线程B (等待者)
A->>Lock: unlock()
activate Lock
Lock->>Lock: tryRelease(1)
Lock->>State: state = state - 1
alt state == 0
State-->>Lock: 返回 true
Lock->>Lock: setExclusiveOwnerThread(null)
Lock->>Queue: unparkSuccessor()
Queue-->>Lock: 找到队首有效等待节点 (线程B)
Lock->>B: LockSupport.unpark(线程B)
deactivate Lock
Note over B: 线程B由内核态挂起转为就绪态
else state > 0
Note over A: 重入锁未完全释放,流程结束
end
rect rgb(255, 245, 235)
Note over C,B: === 非公平锁竞争阶段 ===
C->>Lock: lock() -> CAS 抢锁 (插队)
alt 线程C插队成功
C->>State: CAS 将 0 改为 1
Note over C: 线程C成功获取锁并执行
Note over B: 线程B醒来后抢锁失败,再次 park
else 插队失败
C->>Queue: 入队尾等待
Note over B: 线程B抢锁成功,继续执行
end
end
rect rgb(235, 245, 255)
Note over C,B: === 公平锁竞争阶段 ===
C->>Lock: lock() -> tryAcquire()
C->>Queue: hasQueuedPredecessors()?
Note over C: 检查到B在队列中等待
C->>Queue: 放弃插队,主动加入队尾
Note over B: 线程B被唤醒后无新线程争抢,顺利获得锁执行
end
四、实际应用场景(含完整可运行代码)
场景 1:支持中断的网络请求限流器
需求:控制对第三方 API 的并发请求数为 1(避免触发对方限流),同时允许用户在界面点击“取消”时立即终止等待。
import java.util.concurrent.locks.ReentrantLock;
public class ApiRateLimiter {
private final ReentrantLock lock = new ReentrantLock();
/**
* 模拟调用第三方接口,同一时间只允许一个线程执行。
* 等待锁期间可响应中断。
*/
public String callApi(String param) throws InterruptedException {
// 可中断获取锁
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + " 开始调用API,参数: " + param);
// 模拟网络请求耗时
Thread.sleep(3000);
String result = "Response for " + param;
System.out.println(Thread.currentThread().getName() + " API调用完成");
return result;
} finally {
lock.unlock();
}
}
// 测试可中断特性
public static void main(String[] args) throws InterruptedException {
ApiRateLimiter limiter = new ApiRateLimiter();
// 线程1:先获取锁,执行耗时操作
Thread t1 = new Thread(() -> {
try {
limiter.callApi("request-1");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 被中断,放弃等待");
}
}, "Thread-1");
t1.start();
// 确保 t1 先拿到锁
Thread.sleep(100);
// 线程2:尝试获取锁,会被阻塞
Thread t2 = new Thread(() -> {
try {
limiter.callApi("request-2");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 被中断,放弃等待");
}
}, "Thread-2");
t2.start();
// 主线程等待 1 秒后,中断 t2(模拟用户点击取消按钮)
Thread.sleep(1000);
System.out.println("主线程:用户点击取消,中断 Thread-2");
t2.interrupt();
}
}
输出示例:
Thread-1 开始调用API,参数: request-1
主线程:用户点击取消,中断 Thread-2
Thread-2 被中断,放弃等待
Thread-1 API调用完成
场景 2:数据库连接池的快速失败获取
需求:从连接池获取连接,若池中无空闲连接,立即返回失败,让上层执行降级逻辑(如返回缓存数据)。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
public class ConnectionPool {
private final ReentrantLock lock = new ReentrantLock();
private final List<Connection> pool = new ArrayList<>();
private final int maxSize;
public ConnectionPool(int maxSize) {
this.maxSize = maxSize;
for (int i = 0; i < maxSize; i++) {
pool.add(new Connection("Conn-" + i));
}
}
/**
* 非阻塞获取连接:无空闲连接时立即返回 null
*/
public Connection tryGetConnection() {
if (!lock.tryLock()) {
// 获取锁失败,说明有其他线程正在操作连接池
return null;
}
try {
return pool.isEmpty() ? null : pool.remove(0);
} finally {
lock.unlock();
}
}
/**
* 归还连接
*/
public void releaseConnection(Connection conn) {
lock.lock();
try {
pool.add(conn);
} finally {
lock.unlock();
}
}
// 内部连接类(模拟)
static class Connection {
private final String id;
public Connection(String id) { this.id = id; }
public String getId() { return id; }
}
// 测试快速失败
public static void main(String[] args) {
ConnectionPool pool = new ConnectionPool(2); // 池大小 2
// 线程1 获取一个连接
new Thread(() -> {
Connection conn = pool.tryGetConnection();
System.out.println("Thread-1 获取连接: " + (conn != null ? conn.getId() : "null"));
// 持有连接一段时间,不释放
try { Thread.sleep(2000); } catch (InterruptedException e) {}
pool.releaseConnection(conn);
}).start();
// 线程2 获取第二个连接
new Thread(() -> {
Connection conn = pool.tryGetConnection();
System.out.println("Thread-2 获取连接: " + (conn != null ? conn.getId() : "null"));
try { Thread.sleep(2000); } catch (InterruptedException e) {}
pool.releaseConnection(conn);
}).start();
// 线程3 在池空时尝试获取,应该立即返回 null
new Thread(() -> {
try { Thread.sleep(100); } catch (InterruptedException e) {}
Connection conn = pool.tryGetConnection();
System.out.println("Thread-3 获取连接: " + (conn != null ? conn.getId() : "null(降级处理)"));
}).start();
}
}
输出示例:
Thread-1 获取连接: Conn-0
Thread-2 获取连接: Conn-1
Thread-3 获取连接: null(降级处理)
场景 3:精准唤醒的有界阻塞队列
需求:实现一个固定容量的生产者-消费者队列,队列满时生产者等待,队列空时消费者等待,并且唤醒时只唤醒对方角色,避免惊群效应。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedQueue<T> {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 队列不满条件
private final Condition notEmpty = lock.newCondition(); // 队列不空条件
private final Object[] items;
private int putIndex, takeIndex, count;
public BoundedQueue(int capacity) {
items = new Object[capacity];
}
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
// 队列满,生产者等待
notFull.await();
}
items[putIndex] = item;
if (++putIndex == items.length) putIndex = 0;
count++;
System.out.println(Thread.currentThread().getName() + " 生产: " + item + ", 队列大小: " + count);
// 唤醒一个等待的消费者
notEmpty.signal();
} finally {
lock.unlock();
}
}
@SuppressWarnings("unchecked")
public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
// 队列空,消费者等待
notEmpty.await();
}
T item = (T) items[takeIndex];
items[takeIndex] = null; // help GC
if (++takeIndex == items.length) takeIndex = 0;
count--;
System.out.println(Thread.currentThread().getName() + " 消费: " + item + ", 队列大小: " + count);
// 唤醒一个等待的生产者
notFull.signal();
return item;
} finally {
lock.unlock();
}
}
// 测试生产者-消费者
public static void main(String[] args) {
BoundedQueue<String> queue = new BoundedQueue<>(3); // 容量为 3
// 启动 2 个生产者
for (int i = 0; i < 2; i++) {
final int producerId = i;
new Thread(() -> {
try {
for (int j = 0; j < 5; j++) {
queue.put("P" + producerId + "-" + j);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Producer-" + i).start();
}
// 启动 2 个消费者
for (int i = 0; i < 2; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 5; j++) {
queue.take();
Thread.sleep(200);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Consumer-" + i).start();
}
}
}
输出示例(片段):
Producer-0 生产: P0-0, 队列大小: 1
Producer-1 生产: P1-0, 队列大小: 2
Producer-0 生产: P0-1, 队列大小: 3
Consumer-0 消费: P0-0, 队列大小: 2
Consumer-1 消费: P1-0, 队列大小: 1
Producer-1 生产: P1-1, 队列大小: 2
...
五、吞吐量分析:为什么非公平锁更高?
核心结论:非公平锁通过允许 “两次插队机会” ,显著减少了线程上下文切换(挂起/唤醒)的次数,而上下文切换是并发编程中成本最高的操作之一。
| 对比维度 | 非公平锁 | 公平锁 |
|---|---|---|
| 新线程到达时 | 直接 CAS 抢锁,无视队列 | 检查队列,有等待者则强制入队尾 |
| 释放锁瞬间 | 新线程可与刚唤醒的线程争抢 | 严格将锁交给队首唤醒的线程 |
| 上下文切换次数 | 少(新线程可直接执行,避免唤醒) | 多(每次释放必触发一次唤醒/挂起) |
| CPU 缓存友好度 | 高(新线程通常已在 CPU 上运行,缓存热) | 低(唤醒线程可能被调度到其他核心) |
| 吞吐量(实测) | 约公平锁的 5~10 倍 | 基准值 |
深层原因详述:
- 消除无谓挂起:若锁在极短时间内被释放,新到达的线程立即抢到锁,便避免了 “挂起 → 入队 → 唤醒 → 出队” 的完整流程。一次完整的线程上下文切换在 Linux 下约需 3~5 微秒,高并发下累积开销巨大。
- 利用调度惰性:唤醒一个线程并让其真正开始执行,需要操作系统的调度器介入。而非公平锁允许当前正在 CPU 上运行的“热”线程直接进入临界区,减少了调度延迟。
- 锁释放与获取的时间窗口:当线程 A 释放锁的瞬间(
state变为 0 到唤醒线程 B 之间),存在一个极短的时间窗口。非公平锁允许恰好在此窗口到达的线程 C 直接获取锁,从而避免了线程 B 的一次无效唤醒。 - 实测数据参考:在 JDK 8 环境下,使用
jmh基准测试,100 线程竞争单锁执行微小任务,非公平锁吞吐量可达公平锁的 5~10 倍。
六、注意事项与避坑指南
-
必须显式释放锁:使用
try-finally确保unlock()被执行,否则一旦业务代码抛异常,锁将永不释放,导致系统死锁。java
lock.lock(); try { // 业务逻辑 } finally { lock.unlock(); } -
公平锁谨慎使用:除非业务严格依赖 FIFO 顺序(如任务提交顺序),否则默认非公平锁能提供数倍的吞吐量。公平锁的使用应经过严格的性能评估。
-
临界区不宜过重:
lock()保护的代码块内严禁包含耗时的 I/O 操作、网络请求或复杂计算,否则锁会退化为严重的串行瓶颈,失去并发优势。 -
重入次数有限:重入计数器
state为int类型,上限为Integer.MAX_VALUE(约 21 亿)。递归调用过深导致溢出会抛出Error,需避免无限递归。 -
Condition使用规范:- 必须在
lock.lock()和lock.unlock()之间调用await()和signal()。 - 使用
while循环而非if来检查条件,防止虚假唤醒。 - 优先使用
signalAll()或确保状态变更与唤醒的原子性,避免信号丢失。
- 必须在
-
避免锁的粗化与滥用:仅在必要的最小范围内使用锁,不要为了“省事”而锁住整个方法。过大的锁粒度会严重影响并发性能。
-
注意锁的公平性与性能权衡:在需要保证顺序的场景(如任务调度、公平队列),公平锁是必要的选择;在其他大多数高并发场景下,非公平锁是更好的默认选项。
-
监控与调试:利用
getQueueLength()、hasQueuedThreads()等方法监控锁的竞争情况,及时发现潜在的性能瓶颈。
总结
ReentrantLock 是 AQS 独占模式的经典实现,通过重写 tryAcquire、tryRelease、isHeldExclusively 等钩子方法,实现了可重入、公平性选择、可中断获取、超时等待、多条件变量、状态监控等丰富特性。其非公平锁策略通过减少上下文切换显著提升了吞吐量,而公平锁则保证了严格的 FIFO 顺序。在实际开发中,应根据具体场景选择合适的锁策略,并严格遵循最佳实践,以确保并发程序的正确性与高性能。