一、引言
在多线程编程中,线程安全始终是一个关键议题。Java 在早期版本中提供了synchronized
关键字作为内置锁机制,以支持基本的同步控制。然而,随着并发程序复杂度的提高,synchronized
的局限性日益显现,主要体现在以下几个方面:
-
功能受限:
synchronized
不支持尝试加锁、超时获取、可中断获取等高级功能。 -
缺乏灵活性:一旦进入临界区就只能等待,无法主动退出。
-
可观测性差:开发者无法获知线程是否处于等待队列,也无法干预唤醒策略。
为了解决这些问题,JDK 1.5 引入了 java.util.concurrent
包,引入了基于 AQS(AbstractQueuedSynchronizer) 构建的显式锁机制。其中,ReentrantLock
是最具代表性的实现之一,几乎可看作是 synchronized
的功能超集。
ReentrantLock
提供了以下关键特性,使其在高并发系统中广泛应用:
-
可重入性:同一线程可多次获得同一把锁而不会被阻塞。
-
公平与非公平策略:可控制线程获取锁的排队顺序。
-
可中断获取:支持响应中断机制,提高线程管理灵活性。
-
支持超时尝试获取锁:避免长时间阻塞。
-
条件变量机制(Condition):支持多个等待队列,实现更细粒度的线程协作。
在 Java 8 中,ReentrantLock
的实现更加优化,对性能、吞吐量和可扩展性有显著提升。例如其内部采用了非阻塞算法(CAS)、锁竞争策略优化、线程节点复用等手段来提升效率。
与synchronized
相比,ReentrantLock
虽然语法上更繁琐,但功能更强大、更灵活,尤其适用于如下场景:
-
需要尝试获取锁或定时获取锁
-
需要可中断的加锁过程
-
需要实现多个条件队列
-
对性能或锁的公平性有严格要求
二、ReentrantLock的核心特性
2.1 可重入性
可重入性(Reentrancy)是指同一线程在持有某把锁的情况下,如果再次请求该锁,不会被阻塞。
这是ReentrantLock
名字的由来——它是“可重入锁”。其内部通过一个持有线程引用和一个重入计数器来实现:
public class ReentrantExample {
private final ReentrantLock lock = new ReentrantLock();
public void outer() {
lock.lock();
try {
System.out.println("outer acquired lock");
inner();
} finally {
lock.unlock();
}
}
public void inner() {
lock.lock();
try {
System.out.println("inner also acquired lock");
} finally {
lock.unlock();
}
}
}
输出结果:
outer acquired lock
inner also acquired lock
说明锁是可重入的:同一线程嵌套调用两次lock()
不会死锁。
2.2 公平锁与非公平锁
ReentrantLock
通过构造函数指定公平性策略:
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock unfairLock = new ReentrantLock(); // 非公平锁(默认)
-
公平锁:按照等待线程的先后顺序(FIFO)分配锁,避免插队。
-
非公平锁:允许当前线程插队尝试获取锁,即使其他线程已在排队。
公平锁减少“饥饿”现象,更适合对响应时间有要求的系统;非公平锁可提升吞吐量,适用于高性能场景。
注意:公平性牺牲了一定的性能,因为涉及更多的排队管理逻辑。
2.3 可中断锁
可中断锁允许线程在等待获取锁期间响应中断信号。
使用方式:
lock.lockInterruptibly();
示例:
ReentrantLock lock = new ReentrantLock();
Thread t = new Thread(() -> {
try {
lock.lockInterruptibly();
try {
System.out.println("Thread acquired lock");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("Thread interrupted while waiting for lock");
}
});
t.start();
t.interrupt(); // 可中断
适用于在任务取消时需要退出锁等待队列的场景。
2.4 超时尝试锁
当需要尝试获取锁但不希望长时间阻塞时,可使用带超时机制的tryLock()
方法:
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
// 执行业务逻辑
} finally {
lock.unlock();
}
} else {
System.out.println("未能获取到锁,退出逻辑");
}
该方法可有效防止死锁或长时间阻塞,提高程序健壮性。
此外还有不带参数的重载:
if (lock.tryLock()) {
// 非阻塞获取
}
2.5 条件变量(Condition)
ReentrantLock
配合Condition
实现线程之间的协调等待,功能上类似于synchronized
中的wait()
和notify()
,但支持多个等待队列。
创建方式:
Condition condition = lock.newCondition();
典型用法:
class TaskQueue {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Queue<String> queue = new LinkedList<>();
public void put(String task) {
lock.lock();
try {
queue.offer(task);
notEmpty.signal();
} finally {
lock.unlock();
}
}
public String take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await();
}
return queue.poll();
} finally {
lock.unlock();
}
}
}
通过多个Condition
实例可构建复杂的线程协调机制,如读写队列、阶段控制等,是synchronized
无法实现的。
以上五大特性构成了ReentrantLock
区别于传统内置锁的基础。掌握它们的使用与原理,有助于我们更精细地控制并发逻辑、优化多线程性能。
三、源码深度解析
在 Java 8 中,ReentrantLock
作为 java.util.concurrent.locks
包中的核心组件,底层依赖 AbstractQueuedSynchronizer(AQS) 构建了一个功能强大且灵活的锁实现框架。
3.1 继承结构
ReentrantLock
的核心继承与组合结构如下:
java.util.concurrent.locks.Lock(接口)
↑
java.util.concurrent.locks.ReentrantLock(实现类)
|
Sync(抽象静态内部类,继承AQS)
/ \
NonfairSync FairSync
其中:
-
Lock
:定义了基本的锁操作接口,如lock()
、unlock()
、tryLock()
等。 -
ReentrantLock
:真正的锁实现。 -
Sync
:内部静态抽象类,继承AbstractQueuedSynchronizer
。 -
FairSync
/NonfairSync
:分别实现公平锁与非公平锁逻辑。
核心依赖的是 AQS
的模板方法机制,封装了线程排队、阻塞、唤醒等通用逻辑,子类只需实现锁状态的获取与释放。
3.2 lock()与unlock()方法分析
以非公平锁为例,其lock()
方法逻辑如下:
public void lock() {
sync.lock();
}
对应 NonfairSync
中实现:
final void lock() {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
} else {
acquire(1);
}
}
解释:
-
compareAndSetState(0, 1)
:尝试抢占锁(CAS)。 -
成功则设置当前线程为独占线程。
-
否则进入
AQS.acquire(1)
排队。
释放锁:
public void unlock() {
sync.release(1);
}
调用 AQS 的模板方法:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
核心方法 tryRelease()
在 Sync
中实现,负责清空线程持有者与递减重入次数。
3.3 tryLock()与lockInterruptibly()
tryLock()
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
内部尝试通过 CAS 修改 state 为 1,同时设置独占线程,无需进入阻塞队列。
lockInterruptibly()
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
与lock()
不同的是,当线程被中断时会抛出InterruptedException
,不会继续排队等待。
3.4 AQS核心机制解析
AQS 是构建 ReentrantLock 的底层框架,其核心成员包括:
-
volatile int state
:同步状态。 -
Node head
和Node tail
:双向队列头尾,构建阻塞线程链表。 -
Thread exclusiveOwnerThread
:当前独占线程。
关键流程:
-
加锁失败 → 构造
Node
→ 插入同步队列尾部 → 阻塞线程(LockSupport.park) -
释放锁 →
state
归零 → 唤醒队列中的下一个节点
队列中的每个 Node
代表一个等待获取锁的线程,状态包括 SIGNAL
、CANCELLED
等。
3.5 公平锁与非公平锁差异
非公平锁(默认)
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()) {
setState(c + acquires);
return true;
}
return false;
}
-
当前线程若发现 state=0 立即抢占。
-
支持可重入逻辑:当前线程再次加锁直接累加计数。
公平锁
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;
}
} else if (current == getExclusiveOwnerThread()) {
setState(c + acquires);
return true;
}
return false;
}
公平锁会先检查等待队列中是否有前置节点(即排队线程),有则不抢锁,确保 FIFO 顺序。
四、实际使用示例
通过实战代码演示,进一步巩固对 ReentrantLock
各核心功能的理解。
4.1 基础用法:lock() / unlock()
public class BasicLockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void doSomething() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在执行任务");
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
BasicLockDemo demo = new BasicLockDemo();
Runnable task = demo::doSomething;
new Thread(task, "线程A").start();
new Thread(task, "线程B").start();
}
}
运行结果:两个线程互斥执行任务,保证了线程安全。
4.2 可重入性演示
public class ReentrantDemo {
private final ReentrantLock lock = new ReentrantLock();
public void outer() {
lock.lock();
try {
System.out.println("outer 获得锁");
inner();
} finally {
lock.unlock();
}
}
public void inner() {
lock.lock();
try {
System.out.println("inner 也获得锁");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
new ReentrantDemo().outer();
}
}
控制台输出表明,线程可重复进入同一锁定代码块而不会死锁。
4.3 公平锁与非公平锁对比测试
public class FairLockTest {
private static final ReentrantLock fairLock = new ReentrantLock(true);
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 2; i++) {
fairLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获得锁");
} finally {
fairLock.unlock();
}
}
};
for (int i = 1; i <= 5; i++) {
new Thread(task, "线程" + i).start();
}
}
}
运行输出通常遵循创建线程的顺序(FIFO),证明公平策略生效。
将 new ReentrantLock(true)
改为 false
再测试,可观察到插队现象。
4.4 tryLock() 超时机制
public class TryLockTimeoutDemo {
private final ReentrantLock lock = new ReentrantLock();
public void work(String name) {
try {
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
System.out.println(name + " 成功获取锁");
Thread.sleep(3000);
} finally {
lock.unlock();
}
} else {
System.out.println(name + " 获取锁失败,放弃操作");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public static void main(String[] args) {
TryLockTimeoutDemo demo = new TryLockTimeoutDemo();
new Thread(() -> demo.work("线程A")).start();
new Thread(() -> demo.work("线程B")).start();
}
}
控制台可能输出:
线程A 成功获取锁
线程B 获取锁失败,放弃操作
这体现了 tryLock(timeout)
的非阻塞特性。
4.5 Condition 的多线程协调
public class ConditionDemo {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private boolean ready = false;
public void waitTask() {
lock.lock();
try {
while (!ready) {
System.out.println("等待任务准备...");
condition.await();
}
System.out.println("任务已准备,继续执行");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
public void signalTask() {
lock.lock();
try {
ready = true;
condition.signal();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ConditionDemo demo = new ConditionDemo();
Thread t1 = new Thread(demo::waitTask);
t1.start();
Thread.sleep(1000);
demo.signalTask();
}
}
运行效果:子线程等待,主线程发出信号唤醒。Condition
支持精准唤醒,是复杂线程通信场景的利器。
五、最佳实践与陷阱规避
在使用 ReentrantLock
进行并发控制时,若未注意细节,可能会引发死锁、资源竞争或性能瓶颈等问题。本章总结实战中的常见陷阱与最佳实践。
5.1 避免死锁的策略
死锁通常由多个线程持有锁资源并相互等待造成。以下是常见的防止死锁的策略:
-
固定加锁顺序:确保所有线程按照相同顺序获取多个锁。
-
使用
tryLock()
尝试机制:避免线程长时间阻塞:
if (lock1.tryLock(1, TimeUnit.SECONDS)) {
try {
if (lock2.tryLock(1, TimeUnit.SECONDS)) {
try {
// 操作资源
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
-
资源回收机制:及时释放未成功获取的锁,避免长时间持有部分资源。
5.2 公平锁的性能权衡
公平锁虽然避免了“插队”,但在高并发环境下,频繁的排队与线程上下文切换会导致吞吐量下降:
-
公平锁适用于响应时间敏感场景,如支付系统、交易平台等;
-
非公平锁适合高吞吐优先的服务,如缓存、队列处理等。
建议:优先使用非公平锁,除非业务确有严格公平性需求。
5.3 避免遗漏 unlock()
lock()
和 unlock()
必须成对出现,否则易造成死锁或锁泄露。推荐始终使用try-finally
结构:
lock.lock();
try {
// 临界区
} finally {
lock.unlock();
}
避免如下错误写法:
if (lock.tryLock()) {
// 未释放锁,出错!
}
5.4 ReentrantLock vs synchronized 的选择
特性 | synchronized | ReentrantLock |
---|---|---|
可中断锁 | 否 | 是 |
超时获取锁 | 否 | 是 |
公平锁支持 | 否 | 是 |
条件变量支持 | 否(单一) | 是(多个Condition) |
性能(高并发下) | 一般 | 优 |
使用便捷性 | 简单 | 略繁琐 |
建议:
-
简单场景用
synchronized
,如同步方法、代码块; -
高并发、精细化控制需求用
ReentrantLock
。
5.5 避免锁粒度过大
锁定范围过大会降低并发度,锁定范围过小则增加死锁概率。
建议:
-
将只需要同步的代码移入临界区;
-
拆分对象粒度(如分段锁、分区锁)提高并发。
示例:使用多个锁保护不同资源,提高吞吐:
private final ReentrantLock lockA = new ReentrantLock();
private final ReentrantLock lockB = new ReentrantLock();
public void updateA() {
lockA.lock();
try {
// 仅修改A
} finally {
lockA.unlock();
}
}
public void updateB() {
lockB.lock();
try {
// 仅修改B
} finally {
lockB.unlock();
}
}
通过上述最佳实践的遵守与陷阱的规避,可以显著提升 ReentrantLock
在项目中的稳定性与性能,避免常见并发问题。
六、总结
在并发编程日益成为主流开发场景的今天,ReentrantLock
作为 Java 8 并发工具包中最常用的显式锁之一,提供了比传统 synchronized
更强大和灵活的功能。
回顾全文,我们从其核心特性出发,深入解析了源码实现机制,再到具体的使用案例与实战演练,最后提出了实用的最佳实践建议,旨在帮助开发者全面掌握这一并发利器。
6.1 ReentrantLock 的优势归纳:
-
支持可重入、中断锁、超时获取锁、公平锁策略等高级特性;
-
借助
Condition
实现多个条件队列,提升线程通信的表达能力; -
基于 AQS 构建,具备良好的可扩展性和性能表现;
-
在高并发环境中相比
synchronized
更具性能优势。
6.2 推荐使用场景:
-
对性能和并发控制有精细需求的系统(如缓存、线程池、消息队列);
-
需要响应中断或定时控制的阻塞操作(如网络通信、数据库访问);
-
多线程协作复杂、等待条件不止一类的业务逻辑(适合
Condition
)。
6.3 展望与扩展
尽管 ReentrantLock
功能强大,但在 JDK 8 之后,也引入了新的锁机制如 StampedLock
和 ReadWriteLock
:
-
StampedLock
提供乐观读、悲观写、写升级机制,适合读多写少的场景; -
ReadWriteLock
分离读写锁,提高并发读性能。
后续可针对不同业务特征,灵活选用锁机制,以达成性能与安全的平衡。