ReentrantLock自编
一、lockInterruptibly使用
lock.lockInterruptibly()
是 Java 并发包 (java.util.concurrent.locks
) 中 Lock
接口提供的一个重要方法,它提供了一种可中断的锁获取机制。与基本的 lock()
方法相比,它在锁获取过程中增加了对线程中断(interrupt()
)的响应能力。
核心特性与工作原理
-
可中断的锁获取:
-
当线程尝试通过
lock.lockInterruptibly()
获取锁时:-
如果锁可用,线程会立即获取锁并继续执行(返回)。
-
如果锁不可用,线程会进入等待状态(类似于
synchronized
中的阻塞状态)。
-
-
关键区别:在等待锁的过程中,如果其他线程调用了此线程的
interrupt()
方法,该等待线程会立即响应中断,停止等待锁,并抛出InterruptedException
。
-
-
对比
lock.lock()
:-
普通的
lock.lock()
方法在获取锁时是不可中断的:-
如果锁不可用,线程会一直阻塞等待。
-
即使线程被中断 (
interrupt()
),它依然会继续等待锁,直到成功获取锁。获取锁后,线程的中断状态会被设置(可以通过Thread.interrupted()
检测到),但不会抛出异常。 -
中断操作只在线程成功获取锁之后才能被处理(通过检查中断状态)。
-
-
为什么需要 lockInterruptibly()
?
-
增强响应性:
-
在需要长时间等待锁的场景下(如死锁风险高、资源竞争激烈),
lockInterruptibly()
允许外部通过中断机制主动取消等待锁的线程,避免线程无限期阻塞。 -
这对于实现可取消的任务、超时控制(结合中断)或系统关闭时的资源清理至关重要。
-
-
构建更灵活的同步结构:
-
它是实现
Condition
的await()
方法(可中断的等待)以及其他高级同步工具(如Semaphore.acquire()
)的基础。
-
方法签名
void lockInterruptibly() throws InterruptedException;
-
返回值:
void
-
抛出异常:
InterruptedException
- 当等待锁的线程被中断时抛出。
使用模式
使用 lockInterruptibly()
的标准模式如下:
Lock lock = new ReentrantLock(); // 通常使用 ReentrantLock
try {
// 尝试获取锁(可中断)
lock.lockInterruptibly(); // 可能抛出 InterruptedException
try {
// 临界区代码:访问共享资源
// ...
} finally {
// 无论是否发生异常,确保释放锁
lock.unlock();
}
} catch (InterruptedException e) {
// 处理中断:线程在等待锁时被中断
// 1. 清理状态(如有必要)
// 2. 通常需要恢复中断状态(因为捕获异常会清除中断标志)
Thread.currentThread().interrupt(); // 恢复中断状态
// 3. 根据业务逻辑决定是退出、重试还是其他操作
// 例如:抛出 CancellationException 或直接返回
}
关键注意事项
-
必须在
finally
中释放锁:-
与
lock.lock()
一样,获取锁后必须通过lock.unlock()
释放,且应放在finally
块中确保锁一定被释放,防止死锁。
-
-
正确处理
InterruptedException
:-
不要忽略此异常!这是通知你线程被取消的信号。
-
恢复中断状态:调用
Thread.currentThread().interrupt()
重新设置线程的中断标志。这是 Java 并发编程的最佳实践,因为捕获InterruptedException
会清除中断状态,上层代码可能需要知道中断发生了。 -
执行清理:根据任务性质,可能需要回滚操作、释放其他资源或通知相关组件。
-
决定后续行为:通常是退出当前任务(如抛出
CancellationException
或直接返回)。
-
-
不可中断的锁获取:
-
如果你需要不可中断的锁获取行为,应使用
lock.lock()
。
-
-
与
Condition
配合:-
Lock
创建的Condition
对象(lock.newCondition()
)的await()
方法本身也是可中断的,其行为与lockInterruptibly()
在中断响应上类似。
-
示例:可取消的任务
public class InterruptibleLockExample {
private final Lock lock = new ReentrantLock();
public void performTask() {
try {
lock.lockInterruptibly(); // 可中断地获取锁
try {
System.out.println(Thread.currentThread().getName() + " acquired the lock");
// 模拟长时间工作(期间可被中断)
while (!Thread.currentThread().isInterrupted()) {
// 工作... 这里用 sleep 模拟
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " working...");
}
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + " released the lock");
}
} catch (InterruptedException e) {
// 等待锁或工作期间被中断
System.out.println(Thread.currentThread().getName() + " was INTERRUPTED while waiting or working");
Thread.currentThread().interrupt(); // 恢复中断状态
}
}
public static void main(String[] args) throws InterruptedException {
InterruptibleLockExample example = new InterruptibleLockExample();
Thread worker = new Thread(() -> example.performTask(), "Worker-Thread");
worker.start();
// 主线程等待一会,然后中断工作线程
Thread.sleep(3000);
System.out.println("Main thread interrupting worker...");
worker.interrupt(); // 中断工作线程(无论它在等锁还是工作中)
worker.join();
}
}
总结
lock.lockInterruptibly()
是 Lock
接口提供的核心方法之一,它实现了可中断的锁获取:
-
核心价值:允许线程在等待锁的过程中响应中断请求(
interrupt()
),提高程序的响应性和可控性。 -
与
lock()
的区别:lock()
不可中断,即使被中断也会继续等待锁;lockInterruptibly()
在等待时被中断会抛出InterruptedException
。 -
使用要点:
-
必须用
try-catch
捕获InterruptedException
。 -
必须在
finally
块中释放锁 (unlock()
)。 -
捕获中断异常后,应恢复中断状态 (
Thread.currentThread().interrupt()
) 并执行必要的清理。
-
-
适用场景:需要支持任务取消、超时控制或系统优雅关闭的并发程序。
它是构建健壮、响应灵敏的并发系统的重要工具,尤其在替代传统的 synchronized
+ wait()
/notify()
机制时提供了更强大的中断控制能力。
二、Condition使用
1. ReentrantLock(可重入锁)
含义:
Java 并发包中的互斥锁实现,提供比 synchronized
更精细的控制:
-
可重入性:同一线程可多次获取同一把锁
-
公平性:支持公平锁(先等待的线程先获取)和非公平锁(默认)
-
锁操作:
-
lock()
:阻塞获取锁 -
unlock()
:释放锁(必须在finally
中调用) -
tryLock()
:尝试非阻塞获取锁 -
lockInterruptibly()
:可中断获取锁
-
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区操作
} finally {
lock.unlock(); // 确保释放锁
}
2. Condition(条件变量)
含义:
由 ReentrantLock
创建的线程等待/唤醒控制器:
-
一个锁可创建多个
Condition
(如:notFull
,notEmpty
) -
每个
Condition
管理一个独立的等待队列 -
解决
synchronized
单一等待队列的局限性
Condition condition = lock.newCondition(); // 从锁创建条件
3. await()(条件等待)
含义:Condition
的方法,让线程释放锁并进入等待:
-
调用前提:必须持有锁(否则抛
IllegalMonitorStateException
) -
行为:
-
释放锁
-
线程进入该条件的等待队列
-
被唤醒后重新竞争锁
-
-
必须用
while
循环检查条件(防虚假唤醒)
lock.lock();
try {
while (!conditionMet) { // 循环检查条件
condition.await(); // 释放锁并等待
}
// 条件满足后的操作
} finally {
lock.unlock();
}
4. signal() 与 signalAll()(条件唤醒)
方法 | 含义 | 区别 |
---|---|---|
signal() |
唤醒同一条件队列中的一个线程 | 随机唤醒一个等待线程 |
signalAll() |
唤醒同一条件队列中的所有线程 | 唤醒全部线程竞争锁 |
共同点:
-
调用前提:必须持有锁
-
行为:将等待线程移到锁的同步队列(唤醒后需重新获取锁)
lock.lock();
try {
conditionMet = true; // 改变共享状态
condition.signalAll(); // 唤醒所有等待线程
} finally {
lock.unlock();
}
关键区别对比
组件/方法 | 作用 | 与 synchronized 对比优势 |
---|---|---|
ReentrantLock | 互斥访问共享资源 | 支持公平锁、可中断、多条件 |
Condition | 管理特定条件的等待队列 | 多队列精准控制(如:非满/非空) |
await() | 释放锁并等待条件 | 可绑定特定条件,支持超时等待 |
signal() | 唤醒一个等待线程 | 精确唤醒目标队列的线程 |
signalAll() | 唤醒所有等待线程 | 避免线程饿死(推荐使用) |
为什么使用这种组合?
-
解耦等待条件
-
传统
synchronized
:所有线程在单一等待队列混等 -
Condition
:生产者等notFull
,消费者等notEmpty
-
-
精准唤醒
-
生产者只需唤醒消费者(
notEmpty.signalAll()
) -
避免无效唤醒(如
synchronized
的notifyAll()
会唤醒生产者+消费者)
-
-
高性能
-
减少线程竞争(不同条件独立等待)
-
避免 "惊群效应"(
signal()
精确唤醒一个)
-
最佳实践
-
锁释放规范
lock.lock(); try { // 操作共享资源 } finally { lock.unlock(); // 绝对执行 }
-
条件检查范式
while (!condition) { // 必须用 while,不能用 if! condition.await(); }
-
唤醒策略选择
-
优先
signalAll()
:安全唤醒所有相关线程(推荐) -
谨慎
signal()
:确保不会导致线程饿死
-
-
超时控制
if (condition.await(1, TimeUnit.SECONDS)) { // 正常唤醒 } else { // 超时处理 }
典型应用场景
-
阻塞队列(如
ArrayBlockingQueue
) -
线程池任务调度
-
生产者-消费者系统
-
资源池管理(数据库连接池)
-
有限状态机控制
通过 ReentrantLock + Condition + await()/signal()/signalAll() 的组合,可实现比 synchronized 更高效、更可控的线程协作,尤其适合构建高性能并发数据结构。
3、tryLock()使用
一、tryLock() 的核心概念
tryLock()
是 ReentrantLock
提供的非阻塞式锁获取方法,与常规的 lock()
方法有本质区别:
方法 | 行为特性 | 是否阻塞 | 返回值 |
---|---|---|---|
lock() |
阻塞直到获取锁 | 是 | void |
tryLock() |
尝试立即获取锁 | 否 | boolean |
tryLock(timeout) |
限时尝试获取锁 | 可能阻塞 | boolean |
二、tryLock() 的方法签名
// 立即尝试获取锁(非阻塞)
boolean tryLock();
// 带超时的尝试获取锁(可能阻塞)
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException;
四、典型使用场景
1. 避免死锁(锁顺序获取)
public void transfer(Account from, Account to, int amount) {
while (true) {
if (from.lock.tryLock()) {
try {
if (to.lock.tryLock()) {
try {
from.withdraw(amount);
to.deposit(amount);
return; // 转账成功
} finally {
to.lock.unlock();
}
}
} finally {
from.lock.unlock();
}
}
// 随机退避避免活锁
Thread.sleep(random.nextInt(100));
}
}
2. 快速失败机制
public void processTask() {
if (!lock.tryLock()) {
log.warn("资源忙,跳过本次处理");
return;
}
try {
// 执行关键操作
} finally {
lock.unlock();
}
}
3. 带超时的操作
public void performTimedOperation() {
try {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
// 在500ms内获取到锁
} finally {
lock.unlock();
}
} else {
// 超时处理
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// 中断处理
}
}
五、tryLock() 的特性详解
-
非公平性优先:
-
即使设置为公平锁,
tryLock()
也会忽略公平性原则 -
只要锁可用就立即获取,不考虑等待队列
-
-
可中断性:
-
无参
tryLock()
不可中断 -
带超时的
tryLock()
可响应中断
-
-
重入性支持:
-
与
lock()
一样支持可重入 -
同一线程可多次调用
tryLock()
-
-
与条件变量的关系:
-
tryLock()
不能与Condition
直接配合使用 -
创建
Condition
需要先持有锁
-
六、与 lock() 的对比决策
场景 | 推荐方法 | 原因 |
---|---|---|
必须获取锁才执行操作 | lock() |
保证操作执行 |
避免死锁的锁排序 | tryLock() |
实现锁排序获取 |
高并发下的非关键操作 | tryLock() |
避免阻塞,保持系统响应性 |
需要精确控制等待时间 | tryLock(timeout) |
设置最大等待阈值 |
需要严格公平性的场景 | lock() |
tryLock() 忽略公平性 |
七、最佳实践
-
必须检查返回值
if (lock.tryLock()) { // 必须检查结果! try { /* 操作 */ } finally { lock.unlock(); } } else { // 后备方案 }
-
避免嵌套 tryLock 的活锁
// 错误示例:可能造成活锁 while (!lock1.tryLock() || !lock2.tryLock()) { // 空循环 } // 正确做法:添加随机退避 while (!lock1.tryLock()) { Thread.sleep(random.nextInt(50)); }
-
结合中断处理
try { if (lock.tryLock(1, TimeUnit.SECONDS)) { // ... } } catch (InterruptedException e) { // 恢复中断状态 Thread.currentThread().interrupt(); // 执行清理操作 }
-
资源清理保证
boolean acquired = false; try { acquired = lock.tryLock(); if (acquired) { // 操作资源 } } finally { if (acquired) lock.unlock(); // 仅当获取成功才释放 }
八、性能考量
-
高竞争场景:
-
频繁的
tryLock()
调用可能增加 CPU 开销 -
在锁竞争激烈时,使用队列策略更高效
-
-
低竞争场景:
-
tryLock()
可减少线程切换开销 -
尤其适合短时临界区操作
-
-
监控建议:
long start = System.nanoTime(); boolean acquired = lock.tryLock(100, TimeUnit.MILLISECONDS); long elapsed = System.nanoTime() - start; if (acquired) { metrics.recordLockWaitTime(elapsed); } else { metrics.recordLockTimeout(); }
九、典型应用案例
-
高并发计数器
class OptimisticCounter {
private final ReentrantLock lock = new ReentrantLock();
private int count;
public void increment() {
if (!lock.tryLock()) {
// 快速路径失败时使用备用策略
synchronized (this) {
count++;
}
return;
}
try {
count++;
} finally {
lock.unlock();
}
}
}
-
资源池实现
class ResourcePool {
private final ReentrantLock lock = new ReentrantLock();
private final Queue<Resource> pool = new LinkedList<>();
public Resource acquire(long timeoutMs) throws TimeoutException {
try {
if (lock.tryLock(timeoutMs, TimeUnit.MILLISECONDS)) {
try {
while (pool.isEmpty()) {
// 等待资源可用...
}
return pool.poll();
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
throw new TimeoutException("获取资源超时");
}
}
十、总结
ReentrantLock.tryLock()
提供了比传统锁获取更灵活的机制:
-
非阻塞特性:立即返回获取结果,避免线程阻塞
-
超时控制:设置最大等待时间,防止无限期等待
-
死锁预防:实现安全的锁排序获取策略
-
系统响应性:保持高负载下的系统响应能力
正确使用 tryLock()
需要:
-
严格检查返回值
-
合理处理失败场景
-
结合超时和中断机制
-
避免活锁问题
在需要精细控制锁获取行为的并发场景中,tryLock()
是实现高性能、高可靠性系统的关键工具之一。