跳到主要内容Java 重入锁(ReentrantLock)核心特性与源码剖析 | 极客日志Javajava
Java 重入锁(ReentrantLock)核心特性与源码剖析
Java 重入锁(ReentrantLock)是 Java 并发包中功能强大的同步工具。相比 synchronized,它支持可重入、公平/非公平模式、可中断等待、超时获取及多条件变量。深入解析其基础用法、核心特性、与 synchronized 的对比,并基于 AQS 和 CAS 机制剖析源码实现原理,涵盖加锁、入队、释放流程及实战最佳实践,帮助开发者在高并发场景下选择合适的同步策略。
雪落无声21 浏览 引言
在多线程编程的世界里,锁是最核心的同步工具之一。Java 从语言层面提供了 synchronized 关键字来实现线程同步,简单而有效。然而,随着并发需求的复杂化,synchronized 的局限性逐渐显现——它无法响应中断、无法设置超时、默认非公平且灵活性不足。为了解决这些问题,Java 在 java.util.concurrent.locks 包中提供了 ReentrantLock(重入锁),一个功能更强大、使用更灵活的锁工具。
本文将带你全方位地认识 ReentrantLock,从基本概念到高级特性,从使用方式到源码剖析,从底层原理到实际应用。无论你是初学者还是希望深入理解并发编程的开发者,相信都能从中获得启发。
第一部分:重入锁基础概念
1.1 什么是重入锁?
重入锁(Reentrant Lock),顾名思义,就是支持重入特性的锁。重入是指:同一个线程在持有锁的情况下,可以多次获取同一把锁而不会被阻塞。
举个例子:如果一个线程已经获得了某个对象的锁,当它再次请求该对象的锁时,会直接成功,而不是死锁等待。这种机制在递归方法调用或嵌套同步块中至关重要。
public class ReentrantExample {
private final Object lock = new Object();
public void methodA() {
synchronized (lock) {
methodB();
}
}
public void methodB() {
synchronized (lock) {
System.out.println("methodB 执行");
}
}
}
ReentrantLock 同样支持这种重入特性,但它提供了比 synchronized 更丰富的功能。
1.2 为什么需要重入锁?
synchronized 作为 Java 内置的关键字,使用简单,由 JVM 自动加锁和解锁,且经过多年的优化(偏向锁、轻量级锁、重量级锁升级),性能已经不逊色于 ReentrantLock。既然如此,为什么还需要 ReentrantLock?
这是因为 ReentrantLock 弥补了 synchronized 的几个功能性缺陷:
| 特性 | synchronized | ReentrantLock |
|---|
| 使用方式 | 关键字,自动释放 | API 调用,需手动释放 |
| 锁获取响应中断 | 不支持 | 支持(lockInterruptibly()) |
| 尝试获取锁 | 不支持 | 支持(tryLock()) |
| 超时获取锁 | 不支持 | 支持(tryLock(long, TimeUnit)) |
| 公平锁 | 非公平 | 可设置公平/非公平 |
| 条件变量 | 每个对象一个等待集 | 一个锁可绑定多个 Condition |
| 获取锁状态 | 无法得知 | 可查询持有线程、等待队列等 |
简单来说,当需要更精细的控制同步行为时,ReentrantLock 是更好的选择。
1.3 ReentrantLock 的基本用法
在深入原理之前,我们先来看看 ReentrantLock 的标准使用模式:
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
lock() 和 unlock() 必须成对出现
- 解锁操作必须放在
finally 块中,确保无论是否发生异常都能释放锁
- 不能在
try 块中调用 lock(),因为 lock() 本身可能抛出异常
第二部分:ReentrantLock 的核心特性
2.1 可重入性
可重入性是 ReentrantLock 命名中的核心特性。它通过计数机制实现:
public class ReentrantDemo {
private final ReentrantLock lock = new ReentrantLock();
public void outer() {
lock.lock();
try {
System.out.println("外层方法获取锁");
inner();
} finally {
lock.unlock();
}
}
public void inner() {
lock.lock();
try {
System.out.println("内层方法再次获取锁");
} finally {
lock.unlock();
}
}
}
内部原理:每个锁关联一个持有线程和一个计数器。当线程第一次获取锁时,计数器置为 1;同一个线程再次获取锁时,计数器递增;每释放一次,计数器递减;当计数器归零时,锁完全释放,其他线程才能获取。
2.2 公平锁与非公平锁
2.2.1 概念解析
- 公平锁(FairSync):线程按照请求锁的先后顺序(FIFO)获取锁,不会产生饥饿现象。
- 非公平锁(NonfairSync):线程在获取锁时,允许"插队",即直接尝试抢占锁,如果抢占成功就直接获得锁,抢占失败才进入队列等待。
ReentrantLock 默认使用非公平锁,但可以通过构造器参数设置为公平锁:
ReentrantLock fairLock = new ReentrantLock(true);
ReentrantLock unfairLock = new ReentrantLock(false);
ReentrantLock defaultLock = new ReentrantLock();
2.2.2 为什么默认非公平锁?
非公平锁虽然可能导致线程饥饿,但性能更高。原因在于:
- 公平锁需要维护严格的排队机制,线程唤醒有开销
- 非公平锁减少了线程的挂起和唤醒次数
- 在高并发场景下,非公平锁的吞吐量通常优于公平锁
2.2.3 源码层面的差异
final void lock() {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
} else {
acquire(1);
}
}
final void lock() {
acquire(1);
}
公平锁的 tryAcquire 方法中多了一个关键判断:
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;
}
}
return false;
}
hasQueuedPredecessors() 检查队列中是否有等待时间更长的线程,确保严格 FIFO。
2.3 可中断锁
synchronized 在等待锁的过程中无法响应中断,而 ReentrantLock 提供了可中断的获取锁方式:
public class InterruptibleDemo {
private final ReentrantLock lock = new ReentrantLock();
public void performTask() throws InterruptedException {
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + " 获得锁");
Thread.sleep(5000);
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws Exception {
InterruptibleDemo demo = new InterruptibleDemo();
Thread t1 = new Thread(() -> {
try {
demo.performTask();
} catch (InterruptedException e) {
System.out.println("线程 1 被中断");
}
});
Thread t2 = new Thread(() -> {
try {
demo.performTask();
} catch (InterruptedException e) {
System.out.println("线程 2 被中断");
}
});
t1.start();
Thread.sleep(100);
t2.start();
t2.interrupt();
}
}
当 t2 在等待锁时被中断,会立即抛出 InterruptedException,从而有机会响应中断,而不是无限阻塞。
2.4 限时等待锁
在实际开发中,无限等待锁可能导致系统死锁或响应延迟。ReentrantLock 提供了带超时的锁获取方法:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TimeoutDemo {
private final ReentrantLock lock = new ReentrantLock();
public boolean tryExecute() {
try {
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " 获得锁");
Thread.sleep(2000);
return true;
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " 获取锁超时");
return false;
}
} catch (InterruptedException e) {
System.out.println("线程被中断");
return false;
}
}
}
tryLock() 还有无参版本:如果锁可用则立即获取,否则立即返回 false,不会阻塞。
2.5 条件变量(Condition)
Condition 将 Object 的 wait()、notify()、notifyAll() 方法分解为不同的条件对象,使得一个锁可以支持多个等待集,实现更精细的线程协作。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedBuffer {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Object[] items = new Object[10];
private int putIndex, takeIndex, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await();
}
items[putIndex] = x;
if (++putIndex == items.length) putIndex = 0;
count++;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await();
}
Object x = items[takeIndex];
if (++takeIndex == items.length) takeIndex = 0;
count--;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
优势:与 synchronized 相比,Condition 可以创建多个等待集,更灵活地控制线程协作。
第三部分:ReentrantLock 与 synchronized 的全面对比
3.1 异同点总结
| 比较维度 | synchronized | ReentrantLock |
|---|
| 实现方式 | JVM 内置关键字 | Java API 实现,基于 AQS |
| 锁释放 | 自动释放(退出同步块) | 手动释放(需 finally 中 unlock) |
| 可重入性 | 支持 | 支持 |
| 公平性 | 非公平 | 可公平可非公平 |
| 响应中断 | 不支持 | 支持(lockInterruptibly) |
| 超时获取 | 不支持 | 支持(tryLock 带超时) |
| 尝试获取 | 不支持 | 支持(tryLock 无参) |
| 条件变量 | 每个对象一个等待集 | 一个锁可多个 Condition |
| 锁状态查询 | 无法查询 | 可查询持有线程、等待队列长度等 |
| 性能 | JDK6 后优化良好 | 高竞争场景表现更优 |
3.2 如何选择?
- 优先使用 synchronized:当同步逻辑简单、不需要高级特性时。它简洁、不易出错,且 JVM 持续优化。
- 需要公平锁:必须保证线程获取锁的顺序时。
- 需要可中断锁:希望避免线程无限期阻塞时。
- 需要超时获取锁:防止死锁或保证响应时间时。
- 需要多个条件变量:生产者 - 消费者模式等复杂协作时。
- 高竞争场景:
ReentrantLock 在高并发下表现更好。
第四部分:ReentrantLock 源码深度剖析
4.1 AQS 基础:重入锁的基石
要理解 ReentrantLock,必须先理解AQS(AbstractQueuedSynchronizer)。AQS 是 Java 并发包的基石,ReentrantLock、Semaphore、CountDownLatch 等工具都基于它实现。
4.1.1 AQS 的核心思想
- volatile int state:同步状态,对于
ReentrantLock,state 表示锁的持有次数(0 表示未持有,≥1 表示持有次数)。
- FIFO 等待队列(CLH 队列变体):用于存放获取锁失败的线程。
核心操作:通过 CAS(Compare And Swap)原子性地修改 state 值,成功则获得锁,失败则进入等待队列。
4.1.2 AQS 的关键方法
| 方法 | 描述 |
|---|
tryAcquire(int arg) | 尝试获取锁,由子类实现 |
tryRelease(int arg) | 尝试释放锁,由子类实现 |
acquire(int arg) | 获取锁的模板方法 |
release(int arg) | 释放锁的模板方法 |
ReentrantLock 内部类 Sync 继承 AQS,并实现了 tryAcquire 和 tryRelease。
4.2 非公平锁源码解析
4.2.1 加锁过程
final void lock() {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
} else {
acquire(1);
}
}
compareAndSetState(0, 1):通过 CAS 尝试将 state 从 0 改为 1。如果成功,表示当前线程直接抢到了锁,设置独占线程为当前线程。
- 如果 CAS 失败(锁已被其他线程持有),调用
acquire(1) 进入 AQS 的获取流程。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这里的 tryAcquire 调用的是 NonfairSync 实现的 nonfairTryAcquire:
final boolean nonfairTryAcquire(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;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
非公平的体现:即使线程已经进入等待队列,在 tryAcquire 阶段仍然会尝试 CAS 抢占,而不是严格排队。
4.2.2 入队等待
如果 tryAcquire 失败,则执行 addWaiter 将当前线程封装成 Node 加入等待队列尾部:
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;
}
}
enq(node);
return node;
}
然后执行 acquireQueued,在队列中自旋等待:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
当线程获取锁失败时,会被 park(挂起),等待前驱线程释放锁时 unpark 唤醒。
4.2.3 释放锁
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;
}
释放锁时递减 state,直到 state 归零才真正释放锁。然后 AQS 会唤醒队列中的下一个节点。
4.3 公平锁源码解析
公平锁的 lock() 方法直接调用 acquire(1),没有抢占尝试:
final void lock() {
acquire(1);
}
公平锁的 tryAcquire 与非公平锁的核心区别在于多了 hasQueuedPredecessors() 判断:
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;
}
}
return false;
}
hasQueuedPredecessors() 判断当前线程之前是否有等待的线程,确保 FIFO 顺序。
4.4 限时获取锁的实现
tryLock(long timeout, TimeUnit unit) 的底层通过 doAcquireNanos 实现:
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
long lastTime = System.nanoTime();
final Node node = addWaiter(Node.EXCLUSIVE);
for (;;) {
nanosTimeout -= System.nanoTime() - lastTime;
if (nanosTimeout <= 0) {
cancelAcquire(node);
return false;
}
if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
}
}
通过 LockSupport.parkNanos 实现限时阻塞,超时后自动唤醒并返回失败。
第五部分:CAS 与 AQS——重入锁的底层基石
5.1 CAS 操作
CAS(Compare And Swap)是并发编程中实现无锁算法的核心技术。它是一条 CPU 原子指令,包含三个操作数:
仅当 V 的值等于 A 时,才将 V 更新为 B,整个过程原子完成。
在 Java 中,Unsafe 类提供了 CAS 操作,ReentrantLock 通过 CAS 修改 AQS 的 state 字段。
5.2 CAS 的 ABA 问题
ABA 问题:线程 1 读取变量值为 A,此时线程 2 将 A 改为 B 再改回 A,线程 1 CAS 时发现仍是 A,于是更新成功。但实际上变量已经被修改过。
解决方案:使用版本号或时间戳。Java 提供了 AtomicStampedReference 来解决 ABA 问题。
5.3 AQS 的设计精髓
- 模板方法模式:定义获取/释放锁的骨架,具体实现由子类完成。
- CLH 队列变体:高效的双向队列管理等待线程。
- 状态依赖:通过
state 表示同步状态。
- 自旋与阻塞结合:短时间内自旋,长时间阻塞,平衡性能。
- LockSupport:提供线程挂起和唤醒的底层支持。
第六部分:实战应用与最佳实践
6.1 标准使用模板
public class SafeCounter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public boolean tryIncrement(long timeout, TimeUnit unit) {
try {
if (lock.tryLock(timeout, unit)) {
try {
count++;
return true;
} finally {
lock.unlock();
}
}
return false;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
}
6.2 监控与调试
ReentrantLock 提供了监控锁状态的方法:
ReentrantLock lock = new ReentrantLock();
System.out.println("锁持有线程:" + lock.getOwner());
System.out.println("等待线程数:" + lock.getQueueLength());
System.out.println("是否被当前线程持有:" + lock.isHeldByCurrentThread());
System.out.println("是否公平锁:" + lock.isFair());
6.3 常见陷阱与注意事项
- 忘记释放锁:必须在 finally 中 unlock。
- 在 try 块内 lock:lock() 可能抛出异常,应该先 lock 再 try。
- 锁的可见性问题:ReentrantLock 保证内存可见性,无需额外 volatile。
- 重入计数溢出:重入次数受 int 范围限制,理论上可达 21 亿次。
- 与 synchronized 混用:不同锁机制之间不互斥,需注意设计。
6.4 性能考量
- 低竞争场景:synchronized 性能略优或持平
- 高竞争场景:ReentrantLock 性能更好
- 公平锁性能低于非公平锁
- 避免在锁内执行耗时操作
结语
ReentrantLock 作为 Java 并发包中的核心工具,以其强大的功能和灵活的机制,成为高并发编程中不可或缺的利器。通过本文的学习,我们深入理解了:
- 可重入性的实现原理
- 公平锁与非公平锁的源码差异
- 可中断、限时等待等高级特性
- AQS 作为底层的核心架构
- 最佳实践与性能考量
掌握 ReentrantLock 不仅仅是学会使用一个类,更是理解 Java 并发编程思想的重要一步。在实际开发中,根据场景选择合适的同步工具,平衡功能与性能,才能写出高质量的多线程程序。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online