Play Open
Loading Please wait Loading Please wait Loading Please wait Loading Please wait Loading Please wait Loading Please wait

深入理解 ReentrantLock 可重入锁

前言:

在 Java 并发编程领域,线程安全始终是核心关注点之一。synchronized关键字作为 JVM 层面的内置锁,曾长期是解决线程安全问题的主要选择。但随着并发场景复杂度提升,其灵活性不足、功能单一的缺陷逐渐凸显。java.util.concurrent.locks.ReentrantLock的出现,为开发者提供了更强大、更灵活的锁机制,成为并发编程中的重要工具。本文将从概念、原理、特性、实践等维度,全面解析 ReentrantLock,助力开发者深入掌握其使用方法与设计思想。

一、什么是可重入锁?从概念到本质

1.可重入锁的定义

可重入锁(Reentrant Lock),又称递归锁,是指同一个线程可以多次获取同一把锁,且不会因自身已持有该锁而产生阻塞的锁机制。这种特性允许线程在持有锁的情况下,再次进入由该锁保护的代码块,无需担心死锁问题。ReentrantLock 是 Java 中可重入锁的典型实现,它实现了java.util.concurrent.locks.Lock接口,不仅具备可重入特性,还扩展了诸多synchronized不具备的高级功能,如公平锁控制、中断支持、超时获取等。

2.可重入特性的底层逻辑

ReentrantLock 的可重入性并非 “自动生效”,而是通过底层状态管理实现的。其核心依赖AbstractQueuedSynchronizer(AQS,抽象队列同步器)的状态变量(state) :

当线程首次获取锁时,AQS 会将state从 0 修改为 1,并记录当前持有锁的线程(exclusiveOwnerThread)。

若该线程再次请求获取同一把锁,AQS 会检测到当前线程就是锁的持有者,此时仅将state的值加 1(即 “重入计数 + 1”),无需重新竞争锁。

线程释放锁时,每调用一次unlock(),state的值就减 1;只有当state减至 0 时,锁才会真正释放,其他线程才能竞争。

二、为什么需要 ReentrantLock

1. 同时支持公平锁与非公平锁

公平锁:锁的分配遵循 “先到先得” 原则,等待时间最长的线程优先获取锁,避免线程饥饿(某线程长期无法获取锁)。

非公平锁:线程请求锁时,会先尝试直接抢占锁(无视等待队列),只有抢占失败才会加入等待队列。非公平锁的并发性能更高(减少队列唤醒开销),是 ReentrantLock 的默认策略。

ReentrantLock 通过构造函数指定锁类型:

// 非公平锁(默认)

ReentrantLock nonFairLock = new ReentrantLock();

// 公平锁(显式指定)

ReentrantLock fairLock = new ReentrantLock(true);

2.支持中断式获取锁

ReentrantLock 的lockInterruptibly()方法允许线程在等待锁时响应中断:若其他线程调用该线程的interrupt()方法,等待锁的线程会抛出InterruptedException,并终止等待,避免线程无限阻塞。

import java.util.concurrent.locks.ReentrantLock;

public class InterruptibleLockDemo {

private final ReentrantLock lock = new ReentrantLock();

public void doTask() {

try {

// 支持中断的锁获取

lock.lockInterruptibly();

System.out.println(Thread.currentThread().getName() + "获取锁,开始执行任务");

// 模拟任务执行(耗时)

Thread.sleep(5000);

} catch (InterruptedException e) {

System.out.println(Thread.currentThread().getName() + "在等待锁时被中断");

Thread.currentThread().interrupt(); // 恢复中断状态,供上层处理

} finally {

// 确保锁释放(需先判断当前线程是否持有锁)

if (lock.isHeldByCurrentThread()) {

lock.unlock();

System.out.println(Thread.currentThread().getName() + "释放锁");

}

}

}

public static void main(String[] args) throws InterruptedException {

InterruptibleLockDemo demo = new InterruptibleLockDemo();

// 线程1先获取锁

Thread thread1 = new Thread(demo::doTask, "Thread-1");

thread1.start();

Thread.sleep(1000); // 确保thread1先持有锁

// 线程2尝试获取锁,随后被中断

Thread thread2 = new Thread(demo::doTask, "Thread-2");

thread2.start();

Thread.sleep(1000);

thread2.interrupt(); // 中断thread2的锁等待

}

}

3.支持超时获取锁

ReentrantLock 的tryLock(long timeout, TimeUnit unit)方法允许线程在指定时间内尝试获取锁:若在超时时间内成功获取锁,返回true;若超时仍未获取锁,返回false,线程可自行处理(如放弃任务、重试等),避免无限阻塞。这一特性在 “避免死锁”“控制任务执行时效” 场景中非常实用。

import java.util.concurrent.TimeUnit;

import java.util.concurrent.locks.ReentrantLock;

public class TimeoutLockDemo {

private final ReentrantLock lock = new ReentrantLock();

public void tryAcquireLockWithTimeout() {

try {

// 尝试在3秒内获取锁

boolean isLocked = lock.tryLock(3, TimeUnit.SECONDS);

if (isLocked) {

System.out.println(Thread.currentThread().getName() + "成功获取锁");

// 模拟任务执行

Thread.sleep(2000);

} else {

System.out.println(Thread.currentThread().getName() + "超时未获取锁,放弃任务");

}

} catch (InterruptedException e) {

Thread.currentThread().interrupt();

} finally {

if (lock.isHeldByCurrentThread()) {

lock.unlock();

System.out.println(Thread.currentThread().getName() + "释放锁");

}

}

}

public static void main(String[] args) {

TimeoutLockDemo demo = new TimeoutLockDemo();

// 线程1先获取锁,执行2秒

new Thread(demo::tryAcquireLockWithTimeout, "Thread-1").start();

// 线程2后尝试获取锁,超时时间3秒(实际等待2秒后成功)

new Thread(demo::tryAcquireLockWithTimeout, "Thread-2").start();

}

}

4.支持多个条件变量(Condition)

ReentrantLock 通过newCondition()方法创建多个Condition对象,每个Condition对应一个独立的等待队列,支持 “精准唤醒”:线程通过condition.await()进入该 Condition 的等待队列;其他线程通过condition.signal()/signalAll()唤醒该队列中的线程,避免无效竞争。

import java.util.concurrent.locks.Condition;

import java.util.concurrent.locks.ReentrantLock;

public class ConditionDemo {

// 用Condition实现“生产者-消费者”中的精准唤醒(先生产后消费)

private final ReentrantLock lock = new ReentrantLock();

private final Condition producerCond = lock.newCondition(); // 生产者等待队列

private final Condition consumerCond = lock.newCondition(); // 消费者等待队列

private int count = 0;

private final int MAX_COUNT = 5; // 队列最大容量

// 生产者方法:生产数据

public void produce() throws InterruptedException {

lock.lock();

try {

// 若队列已满,生产者进入等待

while (count >= MAX_COUNT) {

System.out.println("队列已满,生产者" + Thread.currentThread().getName() + "等待");

producerCond.await(); // 生产者进入producerCond等待队列

}

// 生产数据

count++;

System.out.println("生产者" + Thread.currentThread().getName() + "生产,当前 count=" + count);

// 唤醒消费者(队列有数据了)

consumerCond.signal();

} finally {

lock.unlock();

}

}

// 消费者方法:消费数据

public void consume() throws InterruptedException {

lock.lock();

try {

// 若队列为空,消费者进入等待

while (count <= 0) {

System.out.println("队列为空,消费者" + Thread.currentThread().getName() + "等待");

consumerCond.await(); // 消费者进入consumerCond等待队列

}

// 消费数据

count--;

System.out.println("消费者" + Thread.currentThread().getName() + "消费,当前 count=" + count);

// 唤醒生产者(队列有空间了)

producerCond.signal();

} finally {

lock.unlock();

}

}

public static void main(String[] args) {

ConditionDemo demo = new ConditionDemo();

// 启动2个生产者线程

for (int i = 0; i < 2; i++) {

new Thread(() -> {

while (true) {

try {

demo.produce();

Thread.sleep(1000);

} catch (InterruptedException e) {

Thread.currentThread().interrupt();

}

}

}, "P" + i).start();

}

// 启动2个消费者线程

for (int i = 0; i < 2; i++) {

new Thread(() -> {

while (true) {

try {

demo.consume();

Thread.sleep(1500);

} catch (InterruptedException e) {

Thread.currentThread().interrupt();

}

}

}, "C" + i).start();

}

}

}

5.支持锁状态检测

ReentrantLock 提供了多个方法用于查询锁的状态,方便开发者调试与监控:

isHeldByCurrentThread():判断当前线程是否持有该锁;isLocked():判断锁是否被任何线程持有;getHoldCount():获取当前线程持有该锁的重入次数;getQueueLength():获取等待获取锁的线程数(近似值);hasQueuedThreads():判断是否有线程在等待获取锁。

三、ReentrantLock 的核心操作

1.锁获取与释放方法

方法说明void lock()获取锁,若锁被占用则阻塞void lockInterruptibly()获取锁,允许在等待时被中断boolean tryLock()非阻塞尝试获取锁,立即返回结果boolean tryLock(long timeout, TimeUnit unit)在指定时间内尝试获取锁void unlock()释放锁,通常放在 finally 块中确保执行

2.状态查询方法

方法说明boolean isHeldByCurrentThread()当前线程是否持有该锁boolean isLocked()锁是否被任何线程持有boolean isFair()是否为公平锁

3.条件变量方法

Condition 方法说明void await()当前线程释放锁并进入等待状态,直到被其他线程唤醒或中断void signal()唤醒在此Condition上等待的一个线程void signalAll()唤醒在此Condition上等待的所有线程

四、ReentrantLock 的内部结构

ReentrantLock 通过三个内部类实现锁机制:

1.Sync

继承自 AbstractQueuedSynchronizer(AQS),是公平锁与非公平锁的基类。

tryAcquire(int arg):尝试获取锁(由 FairSync 和 NonfairSync 分别实现公平 / 非公平逻辑);

tryRelease(int arg):尝试释放锁(统一实现,核心是将 AQS 的 state 减 1,直至为 0 时释放锁);

newCondition():创建 Condition 对象(基于 AQS 的 ConditionObject 实现)。

2.NonfairSync

非公平锁实现,其核心逻辑是 “先抢占,再排队”。

线程请求锁时,先尝试通过compareAndSetState(0, 1)直接修改 AQS 的 state(从 0 到 1),若成功则获取锁;

若抢占失败(state 已为 1),判断当前线程是否为锁的持有者:若是,则 state 加 1(重入);

若抢占失败且非持有者,则调用 AQS 的acquire(1),将线程加入等待队列并阻塞。

3.FairSync

公平锁实现,其核心逻辑是 “先排队,再获取”

线程请求锁时,先检查 AQS 的等待队列是否有线程在等待(hasQueuedPredecessors());

若有等待线程,则直接加入队列,不抢占;

若无线程等待,则尝试修改 state 获取锁;若 state 已为 1,再判断是否为当前线程(重入)。

五、synchronized 与 ReentrantLock 的区别

对比维度synchronizedReentrantLock锁的实现JVM 层面的内置锁(监视器锁)JDK 层面的锁(基于 AQS 实现)锁获取 / 释放隐式(JVM 自动处理:进入同步块获取,退出释放)显式(手动调用lock()/unlock(),需在finally中释放)可中断性不可中断(等待锁时无法响应中断)可中断(lockInterruptibly()支持中断)超时机制不支持支持(tryLock(timeout, unit))公平性仅支持非公平锁(无法配置)支持公平 / 非公平锁(构造函数指定)条件变量仅 1 个(基于对象监视器,wait()/notify())支持多个(newCondition()创建,精准唤醒)锁状态检测不支持(无 API 查询锁状态)支持(isLocked()/isHeldByCurrentThread()等)性能JDK 1.6 后与 ReentrantLock 接近,简单场景更优复杂场景(中断、超时、多条件)更优,但有手动释放开销

六、总结

ReentrantLock 是 Java 并发编程中非常重要的工具,提供了比 synchronized 更丰富的锁控制机制。通过合理使用 ReentrantLock 及其条件变量,可以构建高效、可靠的多线程应用程序。在实际开发中,应根据具体场景选择是否使用公平锁、是否使用超时机制等,以优化系统性能与响应能力。

Posted in 14年巴西世界杯
Previous
All posts
Next