深入理解 ReentrantLock:从使用方式到 AQS 底层原理
在 Java 并发编程中,提到锁,很多人第一反应是 synchronized。它简单、好用,而且是 JVM 层面的关键字。
但是在一些更复杂的并发场景下,synchronized 就显得不够灵活了。比如:
我想尝试获取锁,拿不到就直接返回
我想最多等待 3 秒,超过时间就放弃
我想让等待锁的线程可以被中断
我想实现公平锁
我想有多个等待条件队列
这时候,就需要用到 JDK 并发包提供的锁:
java.util.concurrent.locks.ReentrantLock
本文就系统介绍一下 ReentrantLock。
一、什么是 ReentrantLock?
ReentrantLock 中文叫:可重入锁
它是 JDK 提供的一种显式锁,位于:java.util.concurrent.locks.ReentrantLock
它的基本作用和 synchronized 类似,都是为了保证多线程访问共享资源时的线程安全。
最基本的使用方式如下:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void test() {
lock.lock(); // 加锁
try {
System.out.println("执行业务逻辑");
} finally {
lock.unlock(); // 解锁
}
}
}
这里最关键的是:
lock.lock(); 表示获取锁。
lock.unlock(); 表示释放锁。
注意:使用 ReentrantLock 时,一定要把 unlock() 放在 finally 中。
否则业务代码一旦抛异常,锁可能无法释放,其他线程就会一直等待。
二、为什么叫“可重入锁”?
所谓可重入,就是:同一个线程已经拿到锁之后,还可以再次获取同一把锁,不会把自己阻塞住。
举个例子:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantDemo {
private final ReentrantLock lock = new ReentrantLock();
public void methodA() {
lock.lock();
try {
System.out.println("methodA");
methodB();
} finally {
lock.unlock();
}
}
public void methodB() {
lock.lock();
try {
System.out.println("methodB");
} finally {
lock.unlock();
}
}
}
执行流程如下:
线程 T1 调用 methodA()
线程 T1 第一次获取锁,state = 1
线程 T1 在 methodA 中调用 methodB()
线程 T1 第二次获取同一把锁,state = 2
methodB 执行完,释放一次锁,state = 1
methodA 执行完,再释放一次锁,state = 0
同一个线程可以多次进入同一把锁,这就是可重入。
但是要注意:加锁几次,就必须解锁几次。
三、ReentrantLock 的核心特性
reentrantLock 主要有以下几个特点:
1. 可重入
2. 可手动加锁和释放锁
3. 支持公平锁和非公平锁
4. 支持 tryLock 尝试加锁
5. 支持超时等待
6. 支持可中断等待
7. 支持多个 Condition 条件队列
8. 底层基于 AQS 实现
四、ReentrantLock 基本使用
1. 普通加锁
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
这里 count++ 不是原子操作,它大概可以分成三步:
1. 读取 count
2. count + 1
3. 写回 count
如果多个线程同时执行,可能出现线程安全问题。
使用 ReentrantLock 后,同一时间只有一个线程可以执行 count++。
五、公平锁和非公平锁
1. 非公平锁
默认创建方式:
ReentrantLock lock = new ReentrantLock();
等价于 ReentrantLock lock = new ReentrantLock(false);
非公平锁的特点是:新来的线程可以直接抢锁,不一定按照排队顺序获取锁。
比如现在 AQS 队列里已经有线程 B、C 在等待:
head -> 线程B -> 线程C
此时锁刚好释放,线程 D 刚好来了。
在非公平锁中,线程 D 可以直接尝试 CAS 抢锁。
如果抢成功:
线程 D 插队成功
线程 B 继续等待
非公平锁的优点:
性能高
吞吐量高
线程切换较少
缺点:
不够公平
可能导致某些线程等待时间较长
2. 公平锁
创建公平锁:
ReentrantLock lock = new ReentrantLock(true);
公平锁的特点是:线程获取锁时要按照队列顺序,先来的线程先获取锁。
也就是说,如果队列中已经有人等待,新来的线程不能插队。
底层会判断:hasQueuedPredecessors()
当前线程前面是否有其他线程在排队?
如果前面有人,就不能直接抢锁。
公平锁的优点:
更公平
不容易出现线程长期等待
缺点:
性能比非公平锁低
线程上下文切换更多
吞吐量较低
实际开发中,大多数情况下使用默认的非公平锁即可。
六、tryLock:尝试获取锁
if (lock.tryLock()) {
try {
System.out.println("获取锁成功,执行业务");
} finally {
lock.unlock();
}
} else {
System.out.println("获取锁失败,直接返回");
}
tryLock() 的含义是:
能拿到锁就执行
拿不到锁就立即返回 false
不会一直阻塞等待
这个在实际业务中非常有用。
比如防止重复提交:
public String submitOrder(Long userId) {
boolean locked = lock.tryLock();
if (!locked) {
return "操作太频繁,请稍后再试";
}
try {
// 创建订单
return "下单成功";
} finally {
lock.unlock();
}
}
如果多个请求同时进来,只有一个请求能拿到锁,其他请求直接返回,避免一直等待。
2. 等待指定时间
tryLock 还可以指定等待时间:
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
System.out.println("3 秒内获取锁成功");
} finally {
lock.unlock();
}
} else {
System.out.println("等待 3 秒仍然没有获取锁,直接失败");
}
完整示例:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void doBusiness() {
boolean locked = false;
try {
locked = lock.tryLock(3, TimeUnit.SECONDS);
if (!locked) {
System.out.println("系统繁忙,请稍后再试");
return;
}
System.out.println("获取锁成功,执行业务逻辑");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("线程被中断");
} finally {
if (locked) {
lock.unlock();
}
}
}
}
七、lockInterruptibly:可中断加锁
普通的 lock() 如果获取不到锁,会一直等待。
lock.lock();
如果线程在等待锁的过程中被中断,它不会立刻响应中断。
而 lockInterruptibly() 支持在等待锁的过程中响应中断。
示例:
import java.util.concurrent.locks.ReentrantLock;
public class InterruptLockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void doWork() {
try {
lock.lockInterruptibly();
try {
System.out.println("获取锁成功,执行业务");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("等待锁时被中断");
}
}
}
适合场景:
任务可以取消
线程等待时间不可控
不希望线程一直卡死在获取锁的过程
八、Condition 条件队列
ReentrantLock 还可以配合 Condition 使用。
Condition 类似于 synchronized 中的:
wait()
notify()
notifyAll()
但是 Condition 更灵活,因为一把锁可以创建多个条件队列。
1. 基本用法
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionDemo {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void awaitMethod() throws InterruptedException {
lock.lock();
try {
System.out.println("线程进入等待");
condition.await();
System.out.println("线程被唤醒");
} finally {
lock.unlock();
}
}
public void signalMethod() {
lock.lock();
try {
System.out.println("唤醒等待线程");
condition.signal();
} finally {
lock.unlock();
}
}
}
注意:condition.await();
会做两件事:
1. 当前线程释放锁
2. 当前线程进入 Condition 等待队列
当其他线程调用:
condition.signal();
等待线程会被唤醒,然后重新进入 AQS 队列竞争锁。
2. Condition 和 wait/notify 的区别

九、ReentrantLock 和 synchronized 的区别

十、ReentrantLock 底层原理
ReentrantLock 底层是基于 AQS 实现的。
AQS 全称:AbstractQueuedSynchronizer(抽象队列同步器)
AQS 的核心可以总结成一句话: AQS = state + CAS + FIFO 队列 + park/unpark
也就是:
state:表示锁状态
CAS:保证修改 state 的原子性
FIFO 队列:保存抢锁失败的线程
park:阻塞线程
unpark:唤醒线程
十一、state 在 ReentrantLock 中的含义
AQS 中有一个核心变量:
private volatile int state;
在 ReentrantLock 中,state 表示锁被重入的次数。
state = 0:锁没有被线程持有
state = 1:锁被某个线程持有一次
state = 2:同一个线程重入两次
例如:
lock.lock(); // state = 1
lock.lock(); // state = 2
lock.unlock(); // state = 1
lock.unlock(); // state = 0
所以,ReentrantLock 可重入的本质就是:
同一个线程再次获取锁时,state 会累加。
十二、ReentrantLock 加锁流程
以非公平锁为例。
当线程调用:
lock.lock();
底层大致流程如下:
1. 线程先尝试 CAS 把 state 从 0 改成 1
2. 如果修改成功,说明获取锁成功
3. 设置当前线程为锁持有者
4. 如果 state 不为 0,判断持有锁的线程是不是当前线程
5. 如果是当前线程,说明是重入,state + 1
6. 如果不是当前线程,说明获取锁失败
7. 当前线程被封装成 Node 节点进入 AQS 队列
8. 当前线程通过 LockSupport.park() 阻塞
伪代码理解:
if (state == 0) {
if (compareAndSetState(0, 1)) {
owner = 当前线程;
return;
}
}
if (owner == 当前线程) {
state++;
return;
}
进入 AQS 队列等待;
队列大概这样:
head -> 线程B -> 线程C -> 线程D
抢锁失败的线程会排队等待。
十三、ReentrantLock 解锁流程
当线程调用:
lock.unlock();
底层大致流程如下:
1. 判断当前线程是不是锁持有者
2. 如果不是,抛出 IllegalMonitorStateException
3. 如果是,state - 1
4. 如果 state 不等于 0,说明只是释放了一层重入锁
5. 如果 state 等于 0,说明锁彻底释放
6. 清空锁持有者
7. 唤醒 AQS 队列中的后继线程
伪代码理解:
if (owner != 当前线程) {
throw new IllegalMonitorStateException();
}
state--;
if (state == 0) {
owner = null;
唤醒下一个等待线程;
}
所以:只有当 state 减到 0,锁才真正释放。
十四、AQS 队列和线程阻塞唤醒
当线程获取锁失败时,会被包装成一个 Node 节点,加入 AQS 队列。
head -> Node(Thread-B) -> Node(Thread-C) -> Node(Thread-D)
每个 Node 大概包含:
thread:当前线程
prev:前一个节点
next:后一个节点
waitStatus:等待状态
然后线程会被阻塞:
LockSupport.park();
当持锁线程释放锁时,会唤醒后继线程:
LockSupport.unpark(thread);
注意:
被唤醒的线程不是直接获得锁,而是重新尝试获取锁。
也就是说:
唤醒 ≠ 拿到锁
唤醒之后还要竞争锁
十五、ReentrantLock 的常见使用场景
1. 防止重复提交
public class SubmitService {
private final ReentrantLock lock = new ReentrantLock();
public String submit() {
if (!lock.tryLock()) {
return "请勿重复提交";
}
try {
// 执行业务逻辑
return "提交成功";
} finally {
lock.unlock();
}
}
}
2. 限时等待资源
public String handle() {
boolean locked = false;
try {
locked = lock.tryLock(2, TimeUnit.SECONDS);
if (!locked) {
return "系统繁忙,请稍后再试";
}
return "处理成功";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "请求被中断";
} finally {
if (locked) {
lock.unlock();
}
}
}
4. 需要公平性的业务场景
比如某些资源分配系统:
ReentrantLock lock = new ReentrantLock(true);
不过要注意,公平锁会降低吞吐量,一般业务不建议随便使用。
总结:
ReentrantLock 底层通过 AQS 的 state 变量记录锁的重入次数。当 state 为 0 时表示锁没有被持有,线程通过 CAS 把 state 从 0 改成 1 后获取锁成功。如果同一个线程再次获取锁,会判断当前线程是否是锁持有者,如果是,就把 state 加 1。释放锁时 state 减 1,只有当 state 减到 0 时,锁才真正释放。
ReentrantLock 底层通过 AQS 的 state 变量记录锁的重入次数。当 state 为 0 时表示锁没有被持有,线程通过 CAS 把 state 从 0 改成 1 后获取锁成功。如果同一个线程再次获取锁,会判断当前线程是否是锁持有者,如果是,就把 state 加 1。释放锁时 state 减 1,只有当 state 减到 0 时,锁才真正释放。
synchronized 是 Java 关键字,底层由 JVM Monitor 实现,使用简单,不需要手动释放锁。ReentrantLock 是 JDK 提供的类,底层基于 AQS 实现,需要手动 lock 和 unlock。相比 synchronized,ReentrantLock 更灵活,支持公平锁、tryLock、超时等待、可中断等待和 Condition 条件队列。但是使用时必须在 finally 中释放锁,否则可能导致死锁。
公平锁是线程获取锁时按照 AQS 队列顺序来,先等待的线程优先获取锁;非公平锁允许新来的线程直接尝试 CAS 抢锁,可能出现插队。ReentrantLock 默认是非公平锁,因为非公平锁减少了线程切换,性能更高。公平锁更公平,但吞吐量相对低。
ReentrantLock 底层基于 AQS 实现。AQS 内部维护了一个 volatile 的 state 变量和一个 FIFO 等待队列。线程加锁时会先通过 CAS 修改 state,如果修改成功则获取锁;如果失败,就封装成 Node 节点加入 AQS 队列,并通过 LockSupport.park 阻塞。释放锁时,state 减 1,当 state 为 0 时表示锁释放,然后通过 LockSupport.unpark 唤醒队列中的后继线程。
普通同步场景优先 synchronized;
复杂并发控制场景可以使用 ReentrantLock。
更多推荐



所有评论(0)