Java 中的 Lock 接口与 ReentrantLock 使用详解

2025-08-07 22:40:38 7632

在并发编程中,我们常用 synchronized 来实现线程同步。但在更复杂的场景下,Lock 接口提供了更强大而灵活的控制方式,特别是它的实现类 —— ReentrantLock。本文将带你全面掌握它的原理、用法和注意事项。

一、为什么需要 Lock 接口?

synchronized 虽然简单,但它存在以下局限:

不能中断等待线程

不能实现超时锁

不能尝试获取锁

不支持公平性策略

无法实现条件变量

而 Lock 接口弥补了这些不足,提供了可中断、公平、尝试获取等高级功能,成为并发控制的重要工具。

二、Lock 接口核心方法

public interface Lock {

void lock(); // 获取锁(可能阻塞)

void lockInterruptibly() // 可响应中断的获取锁

boolean tryLock(); // 尝试获取锁(立即返回)

boolean tryLock(long time, TimeUnit unit); // 超时获取锁

void unlock(); // 释放锁

Condition newCondition(); // 创建条件变量

}

这些方法使得锁的控制不再是“黑箱”,而是完全由开发者掌控。

三、ReentrantLock 的基本用法

Lock lock = new ReentrantLock();

try {

lock.lock();

// 临界区代码

} finally {

lock.unlock();

}

必须使用 try-finally 保证锁最终释放,避免死锁风险。

四、ReentrantLock 实现原理简析

可重入性

所谓可重入,即一个线程获得锁之后,可以再次获得该锁而不会被阻塞。这是通过内部的 holdCount 计数器实现的。

内部机制

ReentrantLock 基于 AQS(AbstractQueuedSynchronizer) 实现。

AQS 维护一个 CLH 队列,通过 CAS 操作控制锁状态。

公平/非公平策略通过 AQS 的不同实现控制。

五、lock vs synchronized 对比

特性

synchronized

ReentrantLock

可重入

公平锁

中断响应

条件变量

✅(Condition)

自动释放锁

❌(必须手动 unlock)

六、Condition 条件变量用法

Lock lock = new ReentrantLock();

Condition condition = lock.newCondition();

lock.lock();

try {

while (!条件成立) {

condition.await(); // 当前线程等待

}

// 条件满足,执行后续逻辑

} finally {

lock.unlock();

}

使用 signal() 或 signalAll() 通知等待线程继续执行,类似 Object.wait/notify,但更灵活。

七、实践建议与最佳实践

一定要在 finally 中释放锁,防止死锁。

只在必要时用 Lock,简单场景优先用 synchronized。

使用 tryLock() 避免长期阻塞,提升系统健壮性。

利用 Condition 拆分多个等待队列,提升控制精度。

八、可视化理解:ReentrantLock 的线程阻塞流程

ReentrantLock 通过链式队列将等待线程串联起来,确保公平与顺序执行。

九、实战场景:银行转账线程安全示例

public void transfer(Account from, Account to, int amount) {

Lock lock1 = from.getLock();

Lock lock2 = to.getLock();

// 避免死锁的锁定顺序控制

if (System.identityHashCode(lock1) < System.identityHashCode(lock2)) {

lock1.lock(); lock2.lock();

} else {

lock2.lock(); lock1.lock();

}

try {

from.withdraw(amount);

to.deposit(amount);

} finally {

lock2.unlock();

lock1.unlock();

}

}

十、ReentrantLock 底层源码:AQS 架构核心

ReentrantLock 是基于 Java 并发包中的核心抽象类 AbstractQueuedSynchronizer(AQS)构建的。

AQS 核心设计理念:

使用一个 volatile int 变量(state)来表示锁状态,并通过一个 CLH 队列管理所有获取锁失败的线程。

关键成员变量:

abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer {

private volatile int state; // 锁状态:0 = 未锁,>0 = 被锁

private transient volatile Node head; // 队列头

private transient volatile Node tail; // 队列尾

}

十一、ReentrantLock.lock() 的执行流程源码解读

public void lock() {

sync.lock(); // sync 为内部 Sync 实例,公平或非公平子类

}

以非公平锁为例:

static final class NonfairSync extends Sync {

final void lock() {

if (compareAndSetState(0, 1)) // 尝试抢占锁

setExclusiveOwnerThread(Thread.currentThread());

else

acquire(1); // 加入等待队列

}

}

AQS.acquire()

public final void acquire(int arg) {

if (!tryAcquire(arg) &&

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

selfInterrupt(); // 响应中断

}

十二、锁的排队机制(CLH 队列)结构解剖

AQS 使用一个双向链表模拟等待线程队列:

head -> Node(Thread-1) <-> Node(Thread-2) <-> ... <-> tail

当线程竞争失败时,进入队列:

private Node addWaiter(Node mode) {

Node node = new Node(Thread.currentThread(), mode);

Node pred = tail;

if (pred != null) {

node.prev = pred;

if (compareAndSetTail(pred, node)) {

pred.next = node;

return node;

}

}

// 若失败则 full enqueue

enq(node);

}

十三、tryAcquire() 的实现

protected final boolean tryAcquire(int acquires) {

final Thread current = Thread.currentThread();

int c = getState();

if (c == 0) {

// 未上锁,尝试竞争

if (compareAndSetState(0, acquires)) {

setExclusiveOwnerThread(current);

return true;

}

} else if (current == getExclusiveOwnerThread()) {

// 可重入

int nextc = c + acquires;

setState(nextc);

return true;

}

return false;

}

十四、Condition 条件变量:底层实现与源码分析

ConditionObject 是 AQS 的内部类,用于管理等待队列。

public class ConditionObject implements Condition {

private transient Node firstWaiter; // 条件队列头

private transient Node lastWaiter; // 条件队列尾

public final void await() throws InterruptedException {

Node node = addConditionWaiter();

int savedState = fullyRelease(node); // 释放锁

boolean interrupted = false;

// 阻塞直到被 signal

while (!isOnSyncQueue(node)) {

LockSupport.park(this);

if (Thread.interrupted()) interrupted = true;

}

acquireQueued(node, savedState); // 恢复锁

if (interrupted) selfInterrupt();

}

public final void signal() {

Node first = firstWaiter;

if (first != null)

doSignal(first);

}

}

使用注意事项:

await() 会释放锁并加入条件等待队列,阻塞线程。

signal() 会将条件队列的线程移至同步队列,并唤醒尝试重新竞争锁。

十五、调试技巧与源码探索建议

1. 打断点追踪 ReentrantLock.lock()

在 IDE(如 IntelliJ IDEA)中跟踪 ReentrantLock.lock() → sync.lock() → AQS.acquire(),可以观察线程在锁竞争中的状态转变。

2. 可视化线程状态

配合 JVM 参数:

-XX:+UnlockDiagnosticVMOptions -XX:+PrintConcurrentLocks

可以使用 jstack 命令分析死锁、锁争用、条件等待等线程状态。

3. JFR(Java Flight Recorder)分析锁竞争

开启 JFR:

java -XX:StartFlightRecording=filename=recording.jfr,duration=60s -jar your-app.jar

使用 JDK Mission Control 可视化分析 Lock 的争用时间和热点代码段。

十六、总结

ReentrantLock 是 Java 并发包的重要基石,而 AQS 则是其强大功能的核心支撑。掌握其源码结构与调试技巧,可以帮助你:

更好理解并发控制的本质

定位性能瓶颈或死锁问题

编写高性能、高安全的并发代码

Copyright © 2022 世界杯积分_上一届世界杯冠军 - f0cai.com All Rights Reserved.