目录
- 什么是 ReentrantLock?
- 为什么需要它?与 synchronized 的核心差异
- 如何使用 ReentrantLock?核心 API 与实战
- 性能考量:ReentrantLock 总是更快吗?
- 最佳实践与常见陷阱
- 总结
什么是 ReentrantLock?
ReentrantLock是 Javajava.util.concurrent.locks包下的一个类,它实现了Lock接口。它的全称是“可重入互斥锁”,这个名字包含了两个关键信息:
- 互斥锁:和
synchronized一样,它在同一时间只允许一个线程持有锁,保证了线程对共享资源访问的原子性。 - 可重入:这是它和
synchronized共同的一个重要特性。一个已经获取到锁的线程,可以再次进入由该锁保护的任何代码块,而不会自己把自己锁死。
可重入示例:
publicclassReentrantExample{privatefinalReentrantLocklock=newReentrantLock();publicvoidouterMethod(){lock.lock();try{System.out.println(Thread.currentThread().getName()+": Outer method");innerMethod();// 在锁内调用另一个需要同一把锁的方法}finally{lock.unlock();}}publicvoidinnerMethod(){lock.lock();try{System.out.println(Thread.currentThread().getName()+": Inner method");}finally{lock.unlock();}}}如果没有“可重入”特性,当outerMethod调用innerMethod时,线程会尝试获取一个它已经持有的锁,从而导致死锁。
为什么需要它?与 synchronized 的核心差异
既然ReentrantLock和synchronized都是可重入的互斥锁,为什么我们还需要ReentrantLock?答案在于它提供了synchronized所不具备的、更强大的功能。
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 锁获取方式 | 隐式获取与释放(JVM 自动管理) | 手动获取与释放(lock()/unlock()) |
| 公平性 | 非公平锁 | 可选择公平或非公平 |
| 可中断锁 | 不可中断,等待线程只能一直等 | 可中断(lockInterruptibly()) |
| 尝试锁 | 不支持 | 支持(tryLock()) |
| 超时锁 | 不支持 | 支持(tryLock(long, TimeUnit)) |
| 绑定条件 | 只有一个条件队列(wait()/notify()) | 可绑定多个条件对象(Condition) |
接下来探讨这些核心优势:
1. 公平性
- 非公平锁(默认):线程获取锁的顺序不一定是它们请求的顺序。JVM 允许“插队”,这可以减少线程上下文切换,提高吞吐量,但可能导致某些线程“饥饿”。
- 公平锁:严格按照线程请求锁的顺序(FIFO)来分配锁。这保证了每个线程都有机会,避免了饥饿,但性能上会有所损失。
// 创建一个公平锁ReentrantLockfairLock=newReentrantLock(true);2. 可中断的锁获取
当一个线程等待synchronized锁时,它只能无限期地等下去。而ReentrantLock提供了lockInterruptibly()方法,允许线程在等待锁的过程中响应中断。
try{lock.lockInterruptibly();// 如果线程在等待时被中断,会抛出 InterruptedExceptiontry{// 临界区代码}finally{lock.unlock();}}catch(InterruptedExceptione){// 处理被中断的情况Thread.currentThread().interrupt();// 恢复中断状态}这在需要优雅取消或超时的任务中非常有用。
3. 尝试锁
tryLock()是一个“非阻塞”的尝试。它会立即返回,告诉你是否成功获取了锁。
if(lock.tryLock()){try{// 成功获取锁,执行临界区代码}finally{lock.unlock();}}else{// 获取锁失败,可以做其他事情,而不是傻等System.out.println("无法获取锁,我先去干点别的...");}tryLock(long time, TimeUnit unit)更进一步,允许在指定时间内等待,超时后自动返回,这是预防死锁的强大武器。
4. 多条件变量
synchronized只能与一个Object的监视器(wait/notify/notifyAll)配合使用,所有等待的线程都在同一个队列里。ReentrantLock可以通过newCondition()创建多个Condition对象,实现更精细的线程分组和唤醒。
想象一个生产者-消费者场景:
Condition notFull:当队列满时,生产者等待此条件。Condition notEmpty:当队列空时,消费者等待此条件。
这样,你可以精确地唤醒“生产者”或“消费者”,而不是像notifyAll()那样无差别地唤醒所有线程。
如何使用 ReentrantLock?核心 API 与实战
使用ReentrantLock的黄金法则是:在finally块中释放锁!这可以确保即使发生异常,锁也总能被释放,避免死锁。
基础用法
classSafeCounter{privateintcount=0;privatefinalReentrantLocklock=newReentrantLock();publicvoidincrement(){lock.lock();try{count++;}finally{lock.unlock();// 必须在 finally 中释放}}publicintgetCount(){lock.lock();try{returncount;}finally{lock.unlock();}}}使用 Condition 实现生产者-消费者
classBoundedBuffer<T>{privatefinalObject[]items;privateintputIndex,takeIndex,count;privatefinalReentrantLocklock=newReentrantLock();privatefinalConditionnotFull=lock.newCondition();privatefinalConditionnotEmpty=lock.newCondition();publicBoundedBuffer(intcapacity){this.items=newObject[capacity];}publicvoidput(Titem)throwsInterruptedException{lock.lock();try{while(count==items.length){notFull.await();// 队列满,等待 notFull 信号}items[putIndex]=item;if(++putIndex==items.length)putIndex=0;++count;notEmpty.signal();// 唤醒一个等待的消费者}finally{lock.unlock();}}@SuppressWarnings("unchecked")publicTtake()throwsInterruptedException{lock.lock();try{while(count==0){notEmpty.await();// 队列空,等待 notEmpty 信号}Titem=(T)items[takeIndex];items[takeIndex]=null;if(++takeIndex==items.length)takeIndex=0;--count;notFull.signal();// 唤醒一个等待的生产者returnitem;}finally{lock.unlock();}}}性能考量:ReentrantLock 总是更快吗?
这是一个经典问题。在 JDK 1.5 时代,ReentrantLock的性能确实远优于synchronized。但随着 JVM 对synchronized的持续优化(引入偏向锁、轻量级锁、自旋锁等),在大多数低竞争场景下,两者的性能差距已经非常小,甚至synchronized略有优势,因为它的语法更简单,JVM 可以进行更深层次的优化。
结论:不要为了性能而选择ReentrantLock。你应该基于功能需求来选择:
- 当你需要公平锁、可中断获取、超时获取或多条件变量时,
ReentrantLock是不二之选。 - 对于简单的同步需求,
synchronized依然因其简洁性和 JVM 的深度优化而是一个很好的选择。
最佳实践与常见陷阱
- 永远在
finally块中unlock():这是最重要的规则,没有之一。 - 保持锁的粒度尽可能小:只在真正需要保护共享资源的代码块上加锁,尽快释放锁,以提高并发度。
- 不要忘记
lock():tryLock()成功后,也要记得在finally中unlock()。 - 避免嵌套锁:和
synchronized一样,在持有锁 A 的情况下再去获取锁 B,如果另一个线程持有锁 B 并尝试获取锁 A,就会发生死锁。 lock()和unlock()必须成对出现:确保代码逻辑中,每一个lock()都有对应的unlock()。
总结
回到我们最初的提问:为什么需要ReentrantLock?
它不仅仅是一个锁,更是一个功能丰富的并发控制工具箱。它将锁的控制权从 JVM 交还给了开发者,让我们能够:
- 选择公平性,在吞吐量和公平性之间做权衡。
- 响应中断,构建更健壮、可取消的任务。
- 尝试与超时,有效预防死锁。
- 精细化唤醒,通过
Condition实现复杂的线程协作。