1 什么是ReentrantLock?:从厕所排队说起
想象一个热门商场的厕所场景:多人同时需要使用,但每次只能容纳一人。这种"独占"访问模式在并发编程中就是典型的互斥访问问题。Java中的ReentrantLock(可重入锁)正是解决这类问题的高效工具,它提供了比传统synchronized关键字更强大、更灵活的锁机制。
官方定义:ReentrantLock是Java并发包(java.util.concurrent.locks)中的可重入互斥锁,具备与synchronized相同的并发性和内存语义,同时集成更多高级功能。
简单类比,ReentrantLock如同智能门禁系统,具备三大核心特性:
- 互斥性:类似厕所门锁,一次仅允许一个线程访问资源
- 可重入性:同一线程可重复获取同一把锁,避免自我阻塞
- 灵活性:支持公平性选择、可中断锁获取、超时机制等高级功能
与synchronized对比,ReentrantLock如同功能全面的"高级门禁系统":synchronized是Java内置关键字,使用简便但功能有限;而ReentrantLock作为完整类,提供更精细的控制能力。
基础用法示例:
ReentrantLock lock = new ReentrantLock(); // 创建非公平锁
// ReentrantLock lock = new ReentrantLock(true); // 创建公平锁
public void criticalSection() {
lock.lock(); // 获取锁
try {
// 临界区代码 - 共享资源访问逻辑
System.out.println("线程" + Thread.currentThread().getName() + "正在操作共享资源");
} finally {
lock.unlock(); // 必须确保释放锁
}
}
注意:lock.unlock()必须置于finally块,确保异常场景下锁仍能释放,避免死锁。
2 ReentrantLock的核心特性:超越基础锁机制
若将synchronized比作普通门锁,ReentrantLock则堪称智能指纹锁,满足多样复杂场景需求。以下对比表格清晰展示两者差异:
表:ReentrantLock与synchronized特性对比
| 特性 |
ReentrantLock |
synchronized |
| 实现层面 |
API层面(JUC包) |
JVM层面(关键字) |
| 锁的获取 |
可尝试、定时、可中断 |
仅支持阻塞等待 |
| 公平性 |
可选公平或非公平锁 |
仅非公平锁 |
| 条件队列 |
可绑定多个Condition |
单一等待池 |
| 释放保证 |
需手动unlock() |
JVM自动释放 |
2.1 可重入性:递归调用的关键保障
可重入性是ReentrantLock的核心特性,允许同一线程多次获取同一把锁而不被阻塞。类比进入房间后需用同一钥匙开启内门,可重入锁确保线程不会被自身阻挡。
技术层面,可重入意味着:同一线程可重复获取同一锁而不引发阻塞,这对递归调用或嵌套方法需同一锁的场景至关重要。
public class RecursiveExample {
private final ReentrantLock lock = new ReentrantLock();
public void outer() {
lock.lock(); // 首次获取锁
try {
inner(); // 调用需同一锁的方法
System.out.println("外部方法执行,锁重入次数: " +
lock.getHoldCount()); // 查看重入次数
} finally {
lock.unlock();
}
}
public void inner() {
lock.lock(); // 再次获取同一锁(重入)
try {
System.out.println("内部方法执行,当前重入次数: " +
lock.getHoldCount());
} finally {
lock.unlock();
}
}
}
若无可重入性,线程在inner()方法中尝试获取锁时将因已持有锁而阻塞,导致死锁。ReentrantLock通过内部计数器追踪重入次数:每次lock()计数器加1,每次unlock()减1,计数器归零时锁真正释放。
2.2 公平性与非公平性:资源分配策略选择
ReentrantLock提供公平与非公平两种锁模式,体现其策略灵活性:
- 公平锁(
new ReentrantLock(true)):遵循先来后到,保证等待最久线程优先获锁
- 非公平锁(
new ReentrantLock(false),默认):允许新线程插队,可能比早等待线程先获锁
性能权衡:公平锁保障公平性但性能较低(线程切换频繁);非公平锁虽不公平但吞吐量更高。多数场景下非公平锁更优,因减少线程切换开销。
2.3 尝试锁与可中断:灵活的资源获取
ReentrantLock提供多样锁获取方式,避免线程无限阻塞:
尝试锁(tryLock):类似等电梯设时限制,超时则执行备选方案
public boolean tryIncrement(long timeout, TimeUnit unit) {
try {
if (lock.tryLock(timeout, unit)) { // 尝试指定时间内获锁
try {
counter++;
return true;
} finally {
lock.unlock();
}
} else {
System.out.println("获取锁超时,执行备用逻辑");
return false;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
可中断锁:等待锁过程可响应中断请求,如排队时接重要电话暂离
public void interruptibleLock() {
try {
lock.lockInterruptibly(); // 可中断获取锁
try {
while (!Thread.currentThread().isInterrupted()) {
// 检查中断状态
}
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("锁获取被中断,优雅退出");
Thread.currentThread().interrupt();
}
}
2.4 条件变量:精细化线程协调
synchronized配合wait()/notify()仅支持单一等待条件;ReentrantLock可创建多个条件变量(Condition),实现更精细线程协调,尤适用于生产者-消费者模型:
public class BoundedBuffer<T> {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 非满条件
private final Condition notEmpty = lock.newCondition(); // 非空条件
private final Object[] items = new Object[100];
private int putptr, takeptr, count;
public void put(T x) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await(); // 等待"非满"条件
}
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal(); // 通知"非空"条件
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 等待"非空"条件
}
T x = (T) items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal(); // 通知"非满"条件
return x;
} finally {
lock.unlock();
}
}
}
通过不同Condition,可精准控制线程唤醒,避免synchronized中notifyAll()引发的"惊群效应"。
3 ReentrantLock的实现原理:深入AQS核心机制
理解ReentrantLock工作原理需先掌握其基石:AQS(AbstractQueuedSynchronizer),即抽象队列同步器。AQS是Java并发包核心框架,ReentrantLock所有功能皆构建其上。
3.1 AQS:同步状态管理引擎
AQS作为同步状态管理器,维护三大关键组件:
- state(状态字段):
volatile int变量,表示锁状态
- 对
ReentrantLock,state=0表示锁空闲
- state>0表示锁占用,数值即重入次数
- 独占线程:记录当前持锁线程
- CLH队列:虚拟双向队列,管理等待线程
AQS采用模板方法模式,定义锁获取与释放骨架,具体逻辑由子类实现,使其成为强大同步框架。
3.2 加锁过程剖析:非公平锁为例
调用lock.lock()时,非公平锁执行流程如下:
// NonfairSync加锁过程
final void lock() {
if (compareAndSetState(0, 1)) { // 1. 先尝试CAS快速获锁
setExclusiveOwnerThread(Thread.currentThread()); // 成功:设当前线程为独占者
} else {
acquire(1); // 2. 失败:进入AQS获取流程
}
}
// AQS的acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 3. 再次尝试获锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 4. 失败后入队阻塞
selfInterrupt();
}
此过程类比医院挂号:
- 直接尝试(插队):新患者(线程)直接问窗口能否挂号(CAS操作)
- 快速成功:若无人挂号(state=0),直接成功免排队
- 正式排队:若窗口有人(state≠0),进入CLH队列
- 队列等待:轮到时再次尝试
非公平锁的tryAcquire实现:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 获取当前状态
if (c == 0) { // 情况1:锁空闲
if (compareAndSetState(0, acquires)) { // CAS尝试获取
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) { // 情况2:重入
int nextc = c + acquires; // 增加重入次数
if (nextc < 0) // 溢出检查
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false; // 获取失败
}
3.3 释放锁过程:唤醒后继线程
释放锁过程侧重状态恢复与唤醒后继线程:
// ReentrantLock的unlock方法
public void unlock() {
sync.release(1); // 委托给AQS的release方法
}
// AQS的release方法
public final boolean release(int arg) {
if (tryRelease(arg)) { // 尝试释放
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒队列下一线程
return true;
}
return false;
}
// Sync的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.4 公平锁 vs 非公平锁的实现差异
公平与非公平锁核心差异体现在tryAcquire方法:
// 公平锁的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 关键区别:多了hasQueuedPredecessors()检查!
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()方法是公平性守护者,检查同步队列中是否有更早等待线程。若有,当前线程不能插队,必须排队。
3.5 正确使用ReentrantLock的注意事项
尽管ReentrantLock功能强大,误用会导致严重问题。以下是关键实践要点:
1. lock()必须在try外部调用
// 正确写法
public void calculate() {
lock.lock(); // lock()在try外部
try {
int result = 100 / 0; // 可能抛出异常
} finally {
lock.unlock();
}
}
// 错误写法(可能导致异常信息被覆盖)
public void calculate() {
try {
lock.lock(); // 错误:lock()在try内部
int result = 100 / 0;
} finally {
lock.unlock();
}
}
2. 必须使用try-finally确保锁释放
public void riskyMethod() {
lock.lock();
try {
dangerousOperation(); // 可能抛出异常
} finally {
lock.unlock(); // 保证锁释放
}
}
3. 避免lock()与try间插入代码
public void problematicMethod() {
lock.lock();
int num = 1 / 0; // 危险:加锁后try前可能异常!
try {
// 临界区代码
} finally {
lock.unlock();
}
}
遵循这些实践可规避常见陷阱,确保ReentrantLock正确使用。
4 实战应用与总结
4.1 实战场景举例
场景1:高性能计数器
public class HighPerformanceCounter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
// 使用tryLock实现非阻塞版本
public boolean tryIncrement() {
if (lock.tryLock()) {
try {
count++;
return true;
} finally {
lock.unlock();
}
}
return false;
}
}
场景2:简单阻塞队列
public class SimpleBlockingQueue<T> {
private final Queue<T> queue = new LinkedList<>();
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
private final int capacity;
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // 等待"非满"条件
}
queue.offer(item);
notEmpty.signal(); // 通知"非空"条件
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 等待"非空"条件
}
T item = queue.poll();
notFull.signal(); // 通知"非满"条件
return item;
} finally {
lock.unlock();
}
}
}
4.2 总结与选型建议
ReentrantLock是Java并发编程关键工具,基于AQS实现高效可重入锁机制。通过源码分析,我们掌握:
- 全局结构:Sync、NonfairSync和FairSync协同工作
- 核心逻辑:state管理锁状态,CAS保证原子性
- 生命周期:初次锁依赖CAS,重入更新state,释放递减state
- 公平性:非公平锁高吞吐,公平锁防饥饿
选型建议:
- 首选
synchronized:简单场景,无需ReentrantLock高级功能时
- 需高级功能时选
ReentrantLock:可中断、超时、公平锁、多条件变量等复杂场景
- 慎用公平锁:公平锁有性能开销,除非必要(如防饥饿),否则用非公平锁
- 确保正确释放:
unlock()必须置于finally块,避免死锁
ReentrantLock提供比synchronized更精细锁控制,是处理复杂并发场景的利器。深入理解其实现原理,有助于编写高效、可靠并发程序。
参考资料
- Java并发编程实战
- Java并发包源码分析
- AQS框架技术文档