java并发包中的ReentrantLock锁详解篇
java并发包中的ReentrantLock锁详解篇
介绍
ReentrantLock
是 Java 并发包 java.util.concurrent.locks
中的互斥锁实现,它提供了比 synchronized
关键字更灵活的锁控制能力,支持 可重入性、公平锁策略、锁中断、超时机制和条件变量特性。
用法用途
1、可重入性**(Reentrant)**
定义
可重入性指的是同一个线程在已经持有某个锁的情况下,可以再次成功获取这个锁,而不会被自己持有的锁所阻塞。
这种“再次获取”的操作会立即成功(或者根据公平策略排队后成功),不会导致线程进入等待状态(不会被自己持有的锁挂起),
(内部通过计数器(hold count
) 实现)。
注意:ReentrantLock 的可重入性严格限定在同一个线程内部。即使是父子线程(比如在父线程中创建并启动的子线程),子线程也无法直接继承或重入父线程持有的 ReentrantLock。这种设计是并发安全的基础。如果锁可以跨线程(如父子线程)重入,会严重破坏锁的互斥性(Mutual Exclusion)保证。想象一下,如果子线程可以随意进入父线程锁定的临界区修改共享数据,而其他线程却被阻塞在外,这将导致数据竞争和不一致,完全违背了使用锁的初衷。锁的持有状态必须清晰地绑定到单个执行线程上,才能有效地协调对共享资源的访问。
想象一下没有可重入性的情况:
- 场景: 线程 A 获取了锁 L。
- 问题: 在线程 A 持有锁 L 期间,如果它执行的代码中(例如,调用了另一个方法)又尝试去获取同一个锁 L。
- 后果(无重入性): 线程 A 会被阻塞在第二次获取锁 L 的操作上!因为它已经在等待一个自己持有的锁。这就造成了经典的死锁——线程 A 永远无法释放锁 L(因为它被阻塞在获取锁的操作上),而其他线程也无法获取锁 L(因为锁还被 A 持有着)。
可重入性就是为了解决这种同一个线程内部嵌套或递归调用需要同一把锁的场景而设计的。它允许线程安全地进入它自己已经锁定的代码区域。
实现原理分析
ReentrantLock
内部维护了两个关键状态:
- 持有者线程(Owner Thread): 记录当前哪个线程持有该锁。
- 持有计数(Hold Count): 记录持有该锁的线程已经成功获取该锁的次数(即重入的深度)。
- 当线程第一次成功获取锁时:
持有者线程
被设置为当前线程。持有计数
被设置为 1。
- 当同一个线程再次成功获取该锁时:
持有者线程
保持不变(还是当前线程)。持有计数
增加 1(例如,变成 2)。
- 当线程调用
unlock()
时:持有计数
减少 1。- 只有当
持有计数
减少到 0 时,锁才会被真正释放(持有者线程
被置为null
),此时其他等待线程才有机会获取该锁。 - 如果
持有计数
大于 0,锁仍然被该线程持有,只是重入深度减少了。必须调用unlock()
的次数与调用lock()
成功的次数严格匹配,才能最终释放锁供其他线程使用
与 synchronized
的比较
Java 内置的 synchronized
关键字实现的锁也是可重入的。ReentrantLock
提供了与 synchronized
相同的可重入语义,但作为显式锁,它提供了更灵活的加锁机制(如可中断、超时、公平锁、多个条件变量等)。
代码示例:
public class ReentrantLockExample {private final ReentrantLock lock = new ReentrantLock();// 一个递归方法,计算阶乘public int factorial(int n) {lock.lock(); // 第一次获取锁 (持有计数 = 1)try {if (n <= 1) {return 1;} else {// 递归调用自身:在同一个线程内,在已经持有锁的情况下再次进入需要锁的方法int result = n * factorial(n - 1); // <--- 这里会发生递归调用!return result;}} finally {lock.unlock(); // 释放锁 (持有计数减1)}}public static void main(String[] args) {ReentrantLockExample example = new ReentrantLockExample();int result = example.factorial(5); // 计算 5!System.out.println("5! = " + result); // 输出: 5! = 120}
}
代码解释如下:
- 调用
factorial(5)
:- 线程(假设是主线程)进入方法。
- 调用
lock.lock()
,成功获取锁。此时:持有者线程
= 主线程持有计数
= 1
- 执行
if (n <= 1)
判断 (5 > 1),进入else
块。 - 计算
5 * factorial(4)
,于是发生递归调用factorial(4)
。
- 进入
factorial(4)
:- 线程(仍然是主线程)再次进入
factorial
方法。 - 再次调用
lock.lock()
。 - 锁检查:
- 当前请求线程 = 主线程
- 当前持有者线程 = 主线程
- 因为是同一个线程再次请求它已经持有的锁,所以允许重入!
- 锁的
持有计数
增加 1(现在 = 2)。 - 执行
if (n <= 1)
判断 (4 > 1),进入else
块。 - 计算
4 * factorial(3)
,递归调用factorial(3)
。
- 线程(仍然是主线程)再次进入
- 递归过程继续:
- 同样的过程发生在
factorial(3)
,factorial(2)
中。每次递归调用factorial(...)
都会:- 遇到
lock.lock()
。 - 因为持有者线程始终是主线程,所以允许重入。
- 锁的
持有计数
不断增加(当进入factorial(1)
时,持有计数
应该达到 5)。
- 遇到
- 同样的过程发生在
- 到达递归基础情况
factorial(1)
:- 执行
if (n <= 1)
判断 (1 <= 1),返回1
。 - 调用
finally
块中的lock.unlock()
。持有计数
减少 1(从 5 变成 4)。- 因为
持有计数
(4) > 0,锁并未真正释放,主线程仍然是持有者。
- 执行
- 递归逐层返回:
- 返回到
factorial(2)
:计算2 * 1 = 2
,然后执行finally
块中的lock.unlock()
。持有计数
减少 1(从 4 变成 3)。
- 返回到
factorial(3)
:计算3 * 2 = 6
,执行unlock()
,持有计数
变成 2。 - 返回到
factorial(4)
:计算4 * 6 = 24
,执行unlock()
,持有计数
变成 1。 - 返回到
factorial(5)
:计算5 * 24 = 120
,执行unlock()
。持有计数
减少 1(从 1 变成 0)。- 因为
持有计数
为 0,锁真正被释放!持有者线程
被置为null
。
- 方法返回结果 120。
- 返回到
2、公平性选择
定义
ReentrantLock 提供了两种工作模式:
- 公平锁(
new ReentrantLock(true)
):按请求顺序分配锁(FIFO)。严格按照线程请求锁的先后顺序分配锁,保证等待时间最长的线程优先获取锁 - 非公平锁(默认):允许插队,吞吐量更高。当锁可用时,任何请求锁的线程(包括新请求和等待队列中的线程)都有机会立即获取锁
示例:
// ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock lock = new ReentrantLock(); // 默认非公平锁lock.lock(); // 阻塞获取锁
try {// 临界区代码
} finally {lock.unlock(); // 必须在finally中释放锁
}
公平锁的实现原理分析
公平锁通过维护一个 FIFO(先进先出)等待队列 实现:
- 当锁被占用时,新请求线程会被加入队列尾部
- 当锁释放时,只会唤醒队列头部的线程(等待时间最长的线程)
- 新线程无法插队,即使锁处于空闲状态也必须排队
非公平锁的实现原理分析
- 当锁释放时,新请求的线程可以"插队"尝试获取锁
- 若获取失败,才会加入等待队列
- 可能导致"线程饥饿":等待队列中的线程可能长期无法获取锁
公平锁和非公平锁性能对比
与 synchronized
的比较
synchronized
仅支持非公平锁。当锁释放时,等待线程和新请求线程会直接竞争锁,不保证先等待的线程优先获取锁。
synchronized
的设计初衷是最大化吞吐量。非公平锁的竞争机制减少了线程上下文切换,性能更高。在 JVM 层面(如 HotSpot 的 ObjectMonitor
),synchronized
的锁获取是抢占式的。没有队列调度机制来保证公平性。
开发者不能通过参数或配置将 synchronized
改为公平锁(而 ReentrantLock
可以通过构造函数指定公平性)。
代码示例
// 创建公平锁和非公平锁private static final ReentrantLock fairLock = new ReentrantLock(true);private static final ReentrantLock nonFairLock = new ReentrantLock();public static void main(String[] args) throws InterruptedException {System.out.println("===== 公平锁测试 =====");testLock(fairLock);Thread.sleep(2000); // 间隔System.out.println("\n===== 非公平锁测试 =====");testLock(nonFairLock);}private static void testLock(ReentrantLock lock) {// 创建5个工作线程for (int i = 1; i <= 5; i++) {new Thread(() -> {for (int j = 0; j < 2; j++) { // 每个线程请求2次锁lock.lock();try {System.out.println(Thread.currentThread().getName() + " 获得锁");Thread.sleep(100); // 模拟工作} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}}, "Thread-" + i).start();// 确保线程按顺序启动try { Thread.sleep(10); }catch (InterruptedException e) {}}}
公平锁可能的输出:特征:线程严格按照启动顺序获得锁(1→2→3→4→5→1→2…)
Thread-1 获得锁
Thread-2 获得锁
Thread-3 获得锁
Thread-4 获得锁
Thread-5 获得锁
Thread-1 获得锁
Thread-2 获得锁
...
非公平锁可能的输出:获取顺序随机,可能出现:
- 同一线程连续获取锁
- 后启动的线程先获取锁
- 某些线程长时间无法获取锁
Thread-1 获得锁
Thread-1 获得锁 // 同一线程连续获取
Thread-3 获得锁
Thread-5 获得锁 // 顺序被打乱
Thread-2 获得锁
...
适用场景
- 需要严格顺序执行:如交易系统需要按订单到达顺序处理
- 避免线程饥饿:关键服务必须保证所有请求都能被处理
- 调试排错:当需要确定锁获取顺序时
3、锁可中断
定义
ReentrantLock 提供了可中断的锁获取机制,这是它与传统 synchronized
关键字的关键区别:
- 线程在等待锁的过程中可以响应中断请求
- 当等待线程被中断时,会立即停止等待并抛出
InterruptedException
- 这允许程序优雅地处理长时间等待的场景
如果不使用 lockInterruptibly()
而使用普通的 lock()
方法,效果会有显著不同,如下代码分析
// 使用普通lock()
lock.lock(); // 即使线程被中断,也会继续等待锁,此处开始阻塞,继续阻塞直到获取锁,
try {// 中断信号被"吞没",只能在获取锁后检测// 获取锁后检查中断状态if (Thread.interrupted()) {System.out.println("获取锁后发现曾被中断");}// ... 业务逻辑 ...
} finally {lock.unlock();
}
- 可能导致:
- 业务逻辑错误执行(本应取消的操作继续执行)
- 资源浪费(继续执行无意义的操作)
- 响应延迟(中断后仍长时间等待锁)
lock()
在阻塞期间对中断完全"免疫",这可能导致线程无法响应取消请求,增加死锁风险,并降低系统健壮性。在设计可取消任务时,应优先考虑 lockInterruptibly()
。
注意以下点:
- 中断不是取消:中断只是请求,具体行为由代码实现决定
与 synchronized
的比较
在需要响应中断、超时控制或死锁恢复的场景,必须使用 ReentrantLock
替代 synchronized
。如下:
例子一:手动中断任务
使用 synchronized
的灾难性结果
ExecutorService executor = Executors.newCachedThreadPool();Future<?> future = executor.submit(() -> {synchronized(lock) { // 阻塞在此处时不可中断System.out.println("获取到锁"); // 永远不会执行!}
});Thread.sleep(1000);
future.cancel(true); // 尝试取消任务 → 失败!// 结果:
// 1. 长时间无法获取锁,线程永远阻塞在synchronized处
// 2. 一直无法获取锁,线程池资源永久占用
// 3. 阻塞很多线程,可能导致线程池耗尽
- 如果线程正在运行或处于
WAITING
/TIMED_WAITING
状态(如Object.wait()
、Thread.sleep()
),中断会触发InterruptedException
。 - 但
synchronized
锁的阻塞是BLOCKED
状态,此时中断仅会设置线程的中断标志位,不会唤醒线程或抛出异常。
使用 ReentrantLock
的正确取消
Future<?> future = executor.submit(() -> {try {lock.lockInterruptibly(); // 可中断点System.out.println("获取到锁");} catch (InterruptedException e) {System.out.println("任务被取消退出");} finally {if (lock.isHeldByCurrentThread())lock.unlock();}
});Thread.sleep(1000);
future.cancel(true); // 成功取消!// 输出: "任务被取消退出"
代码示例
正例
public class BusinessService {private final ReentrantLock queryLock = new ReentrantLock();// 模拟长时间数据库查询public String executeLongQuery(String query) throws InterruptedException {boolean acquired = false;try {queryLock.lockInterruptibly(); // 可中断获取acquired = true;// 执行关键操作...TimeUnit.SECONDS.sleep(10);return "成功";} catch (InterruptedException e) {// 1. 恢复中断状态(保持中断请求的传播)Thread.currentThread().interrupt();// 2. 执行中断清理操作System.out.println("操作被中断,执行清理...");// 3. 返回或抛出合适的业务异常return null;} finally {// 4. 确保只在成功获取锁时才释放if (acquired) {queryLock.unlock();}}}
}
public class ReentrantLockExample {public static void main(String[] args) throws InterruptedException {BusinessService service = new BusinessService();// 创建长时间查询线程Thread longQueryThread = new Thread(() -> {try {String result = service.executeLongQuery("SELECT * FROM huge_table");System.out.println("查询结果: " + result);} catch (InterruptedException e) {System.out.println("【查询被中断】: " + e.getMessage());}}, "查询线程");// 创建中断控制线程Thread controlThread = new Thread(() -> {try {// 等待2秒后中断查询TimeUnit.SECONDS.sleep(2);System.out.println("\n--- 系统发出中断请求 ---");longQueryThread.interrupt(); // 中断查询线程} catch (InterruptedException e) {e.printStackTrace();}}, "控制线程");longQueryThread.start();controlThread.start();longQueryThread.join();controlThread.join();}
}
代码分析:
关键流程:
- 查询线程获取锁并开始执行长时间操作
- 控制线程在2秒后发出中断请求
- 查询线程在
TimeUnit.SECONDS.sleep(10)
处被中断- 抛出
InterruptedException
- 但此时锁仍被持有!
- 抛出
finally
块确保锁被释放- 程序优雅退出,避免资源泄漏
反例1
class DatabaseService {private final ReentrantLock lock = new ReentrantLock();public String executeLongQuery(String query) {lock.lock(); // 普通不可中断锁try {System.out.println(Thread.currentThread().getName() + " 开始查询");// 模拟长时间操作Thread.sleep(10000); // 睡眠10秒return "结果: " + query;} catch (InterruptedException e) {// 注意:这个异常只可能来自Thread.sleep()中断System.out.println("【睡眠被中断】但锁仍然持有");return null;} finally {lock.unlock();System.out.println(Thread.currentThread().getName() + " 释放锁");}}
}
public static void main(String[] args) throws InterruptedException {DatabaseService service = new DatabaseService();Thread worker = new Thread(() -> {System.out.println("查询结果: " + service.executeLongQuery("SELECT * FROM table"));}, "工作线程");worker.start();Thread.sleep(2000); // 等待2秒System.out.println("--- 发送中断请求 ---");worker.interrupt(); // 中断工作线程worker.join(3000); // 等待3秒System.out.println("工作线程状态: " + worker.getState());
}
运行结果:
工作线程 开始查询
--- 发送中断请求 ---
【睡眠被中断】但锁仍然持有
工作线程 释放锁
查询结果: null
工作线程状态: TERMINATED
代码分析:
- 中断响应位置错误:
- 中断只影响了
Thread.sleep()
,没有影响锁等待 - 如果线程在
lock()
处阻塞(而非sleep),中断会被完全忽略
- 中断只影响了
- 锁释放问题:
- 即使业务操作被中断,仍然需要手动释放锁
- 如果忘记释放(如本例中没进try块),会导致死锁
- 状态不一致风险:
- 业务操作被中断后,可能留下部分完成的状态
- 需要额外的回滚逻辑处理中断
反例2
最危险场景:死锁中的中断
考虑两个线程互相等待的场景
ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();Thread thread1 = new Thread(() -> {lockA.lock(); // 普通lock()try {Thread.sleep(100); // 确保thread2获取lockBlockB.lock(); // 此处阻塞(等待thread2释放)} catch (InterruptedException e) {System.out.println("thread1中断 - 但无法退出死锁");} finally {lockA.unlock();}
});Thread thread2 = new Thread(() -> {lockB.lock(); // 普通lock()try {Thread.sleep(100);lockA.lock(); // 此处阻塞(等待thread1释放)} catch (InterruptedException e) {System.out.println("thread2中断 - 但无法退出死锁");} finally {lockB.unlock();}
});
当尝试中断死锁:
thread1.start();
thread2.start();
Thread.sleep(1000);
thread1.interrupt();
thread2.interrupt();
结果:
- 两个线程继续阻塞在对方的锁上
- 中断信号被完全忽略
- 程序永久挂起,无法恢复
💡 使用
lockInterruptibly()
可使线程响应中断退出死锁状态
4、超时机制
定义
支持 tryLock(long timeout, TimeUnit unit)
,在指定时间内尝试获取锁。这是其区别于 synchronized
的关键特性之一,该机制允许线程在指定时间内尝试获取锁,避免无限期阻塞。等待过程中可被中断
代码示例
public class ReentrantLockExample {// 股票资源static class Stock {/*** 存储某个特定操作或标识符的符号*/private final String symbol;/*** 表示当前可用的股份总数*/private int availableShares;private final ReentrantLock lock = new ReentrantLock();public Stock(String symbol, int shares) {this.symbol = symbol;this.availableShares = shares;}// 尝试购买股票(带超时)public boolean tryBuy(String userId, int quantity, long timeout, TimeUnit unit)throws InterruptedException {// 尝试在指定时间内获取锁,如果未能在指定时间内获取到锁,则返回falseif (!lock.tryLock(timeout, unit)) {System.out.printf("[超时] %s 购买 %s 失败:等待时间过长%n", userId, symbol);return false;}try {// 检查是否有足够的股票可供购买if (availableShares < quantity) {System.out.printf("[库存不足] %s 购买 %d 股 %s 失败%n",userId, quantity, symbol);return false;}// 模拟处理时间Thread.sleep(1000);// 更新可用股票数量availableShares -= quantity;System.out.printf("[成功] %s 购买 %d 股 %s,剩余 %d 股%n",userId, quantity, symbol, availableShares);return true;} finally {// 释放锁,确保其他线程可以尝试购买lock.unlock();}}}/*** [成功] 用户-1 购买 300 股 AAPL,剩余 700 股* [超时] 用户-3 购买 AAPL 失败:等待时间过长* [超时] 用户-5 购买 AAPL 失败:等待时间过长* [成功] 用户-2 购买 300 股 AAPL,剩余 400 股** === 系统中断:市场波动过大 ===* 用户-4 交易被中断** @param args* @throws InterruptedException*/public static void main(String[] args) throws InterruptedException {Stock appleStock = new Stock("AAPL", 1000);// 创建多个用户线程Thread[] traders = new Thread[5];for (int i = 0; i < traders.length; i++) {final String userId = "用户-" + (i + 1);final int quantity = 300; // 每人购买300股traders[i] = new Thread(() -> {try {// 设置不同超时时间(1-3秒)long timeout = 1000 + (long) (Math.random() * 2000);appleStock.tryBuy(userId, quantity, timeout, TimeUnit.MILLISECONDS);} catch (InterruptedException e) {System.out.printf("%s 交易被中断%n", userId);}});}// 启动所有交易线程for (Thread trader : traders) {trader.start();Thread.sleep(50); // 错开启动时间}// 模拟系统中断(2秒后中断所有交易)Thread systemControl = new Thread(() -> {try {Thread.sleep(2000);System.out.println("\n=== 系统中断:市场波动过大 ===");for (Thread trader : traders) {if (trader.isAlive()) {trader.interrupt();}}} catch (InterruptedException e) {e.printStackTrace();}});systemControl.start();// 等待所有线程结束for (Thread trader : traders) {trader.join();}}}
运行结果:超时后可直接返回
* [成功] 用户-1 购买 300 股 AAPL,剩余 700 股
* [超时] 用户-3 购买 AAPL 失败:等待时间过长
* [超时] 用户-5 购买 AAPL 失败:等待时间过长
* [成功] 用户-2 购买 300 股 AAPL,剩余 400 股
*
* === 系统中断:市场波动过大 ===
* 用户-4 交易被中断
5、条件变量机制(Condition)
定义
Condition
是 Java 并发包(java.util.concurrent.locks
)中用于线程间协调的机制,通常与 ReentrantLock 配合使用。它的核心功能是允许线程在特定条件未满足时挂起(阻塞),直到其他线程修改条件后唤醒它。
可通过 newCondition()
创建多个条件队列,实现精细化的线程通信。
方法 | 描述 |
---|---|
await() | 释放锁并挂起线程,直到被唤醒或中断。 |
signal() | 唤醒一个等待在该 Condition 上的线程(随机选择)。 |
signalAll() | 唤醒所有等待在该 Condition 上的线程。 |
awaitUninterruptibly() | 不可中断的等待,需手动处理中断。 |
await(long time, TimeUnit unit) | 带超时的等待,超时后自动恢复。 |
与 synchronized
的比较
synchronized
的wait()
和notify()
只能配合Object
的监视器锁使用,且每个对象只有一个等待队列。如下例子
public class ProducerConsumerExample {// 共享资源:缓冲区(容量为1)private final Object lock = new Object();private boolean isProduced = false; // 标记是否已生产数据private int data = 0;// 生产者线程public void producer() {new Thread(() -> {while (true) {synchronized (lock) {// 如果已生产,等待消费者消费while (isProduced) {try {System.out.println("生产者等待...");lock.wait(); // 释放锁并等待} catch (InterruptedException e) {e.printStackTrace();}}// 生产数据data++;System.out.println("生产者生产了数据: " + data);isProduced = true;lock.notify(); // 唤醒消费者}}}).start();}// 消费者线程public void consumer() {new Thread(() -> {while (true) {synchronized (lock) {// 如果未生产,等待生产者生产while (!isProduced) {try {System.out.println("消费者等待...");lock.wait(); // 释放锁并等待} catch (InterruptedException e) {e.printStackTrace();}}// 消费数据System.out.println("消费者消费了数据: " + data);isProduced = false;lock.notify(); // 唤醒生产者}}}).start();}public static void main(String[] args) {ProducerConsumerExample example = new ProducerConsumerExample();example.producer();example.consumer();}
}
Condition
则通过ReentrantLock.newCondition()
创建,支持多个条件变量,可以实现更精细的线程控制(例如区分“队列非空”和“队列未满”两个条件)。具体见代码示例
synchronized 更简单但功能有限;ReentrantLock + Condition 更适合复杂需求。
适用场景
- 生产者-消费者模型:当缓冲区满时,生产者挂起;当缓冲区空时,消费者挂起。
- 线程间任务协调:例如主线程等待多个子线程完成任务后再继续执行。
- 复杂同步逻辑:使用多个
Condition
分离不同的等待条件,避免过早唤醒。
代码示例
注意事项
- 必须持有锁才能调用
await()
/signal()
:- 如果未持有锁直接调用
await()
或signal()
,会抛出IllegalMonitorStateException
。
- 如果未持有锁直接调用
signal()
不会立即唤醒线程:signal()
只是将线程从条件队列移动到锁的同步队列,线程需要重新竞争锁后才能继续执行。
生产者-消费者模型*示例:
当缓冲区满时,生产者挂起;当缓冲区空时,消费者挂起。
public class ReentrantLockExample {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 putPtr = 0, takePtr = 0, count = 0;// 生产者方法public void put(Object 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 Object take() throws InterruptedException {lock.lock();try {// 缓冲区空时等待while (count == 0) {notEmpty.await();}Object x = items[takePtr];if (++takePtr == items.length) takePtr = 0; // 循环指针count--;notFull.signal(); // 唤醒生产者return x;} finally {lock.unlock();}}}
分析代码
notFull
和notEmpty
:两个条件变量分别表示缓冲区未满和非空。await()
:线程在条件不满足时挂起,并释放锁。signal()
:条件满足后唤醒等待的线程。while
循环检查条件:避免虚假唤醒(即使没有其他线程调用signal()
,线程也可能被唤醒,这是底层操作系统和线程调度机制的特性,在 Java 规范中明确允许存在。)。
多条件示例
public class MultiConditionExample {private final ReentrantLock lock = new ReentrantLock();private final Condition condition1 = lock.newCondition();private final Condition condition2 = lock.newCondition();public void awaitCondition1() throws InterruptedException {lock.lock();try {condition1.await(); // 等待条件1} finally {lock.unlock();}// // 可中断等待
// lock.lockInterruptibly();
// try {
// condition1.await(); // 可响应中断
// } catch (InterruptedException e) {
// // 处理中断
// } finally {
// lock.unlock();
// }
//
// // 超时等待
// if (lock.tryLock(1, TimeUnit.SECONDS)) {
// try {
// if (condition1.await(5, TimeUnit.SECONDS)) {
// // 条件满足
// } else {
// // 超时处理
// }
// } finally {
// lock.unlock();
// }
// }}public void signalCondition1() {lock.lock();try {condition1.signal(); // 唤醒条件1的线程} finally {lock.unlock();}}}
进阶:ReentrantReadWriteLock的使用
介绍
ReentrantReadWriteLock
: 将锁分离为读锁和写锁。- 读锁 (
ReadLock
): 是共享锁。多个线程可以同时持有读锁,进行并发读取操作。这大大提高了读操作的并发性。 - 写锁 (
WriteLock
): 是互斥锁/排他锁。同一时刻只能有一个线程持有写锁。当一个线程持有写锁时,其他任何线程(包括试图获取读锁或写锁的线程)都会被阻塞。写锁还互斥所有读锁(即:有写锁时不能有读锁,反之亦然)。
- 读锁 (
在 读远多于写 的场景下,ReentrantReadWriteLock
能带来显著的并发性能提升;而在其他场景下,简单可靠的 ReentrantLock
通常是更安全、更高效的选择。务必根据实际性能测试结果做出最终决定。
锁降级特性
1、定义
做更新操作时获取写锁,其他所有线程(读/写)则被阻塞。执行更新操作,数据成功更新后,在当前线程可以继续获取读锁,此时线程仍然持有写锁,ReentrantReadWriteLock
允许持有写锁的线程获取读锁(这是锁降级的基础,反过来则不行,因为读锁是共享锁),获取读锁成功(计数器增加)。然后释放写锁,线程释放了写锁。此时,该线程只持有读锁。这就是“降级”——从独占的写锁降级为共享的读锁。此时允许其他线程的读操作。
2、适用场景
成功降级到读锁后,由于读锁是共享锁,在读锁未释放其他线程就无法获取写锁来修改数据,其他线程的读操作可以立马读到更新线程更新后的数据,比如需要读取更新后的数据进行记录日志,只要保证读锁未释放时就有其他线程的读操作进行读数据,就正确的记录日志。
如果不进行锁降级,只有释放当前的写锁,其他线程才能获取读写锁进行读写操作。如果想要正确读取之前线程更新的数据可能不准确,因为释放写锁后可能立马有另一个写锁又修改了数据。