1. ReentrantLock基础概念解析
在Java并发编程中,ReentrantLock作为synchronized关键字的替代方案,提供了更灵活的线程同步机制。我第一次接触ReentrantLock是在处理一个高并发的订单系统时,当时synchronized的性能瓶颈迫使我寻找更优的解决方案。
ReentrantLock属于java.util.concurrent.locks包下的显式锁实现,与内置锁synchronized相比具有以下特点:
- 可重入性:同一个线程可以多次获取同一把锁
- 可中断:支持lockInterruptibly()方法实现可中断的锁获取
- 公平性:支持公平锁和非公平锁两种模式
- 条件变量:通过newCondition()方法创建多个条件队列
重要提示:使用ReentrantLock时必须手动释放锁,否则会导致死锁。建议始终在finally块中释放锁。
2. 核心API与基础用法
2.1 基本锁操作示例
ReentrantLock lock = new ReentrantLock(); public void criticalSection() { lock.lock(); // 获取锁 try { // 临界区代码 System.out.println(Thread.currentThread().getName() + "进入临界区"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); // 必须在finally中释放锁 System.out.println(Thread.currentThread().getName() + "释放锁"); } }这个基础示例展示了最核心的lock()/unlock()用法。我在实际项目中犯过的典型错误包括:
- 忘记在finally中释放锁,导致死锁
- 在不同方法中获取和释放锁,破坏了锁的对称性
- 嵌套调用时没有注意重入次数
2.2 可中断锁获取
public void interruptibleLock() { try { lock.lockInterruptibly(); // 可响应中断的锁获取 try { // 临界区代码 } finally { lock.unlock(); } } catch (InterruptedException e) { // 处理中断逻辑 Thread.currentThread().interrupt(); } }这种模式特别适合需要快速响应中断的场景,比如服务关闭时的资源释放。我在网关服务中就用这种方式实现了优雅停机。
3. 高级特性详解
3.1 公平锁与非公平锁
// 公平锁(按申请顺序获取) ReentrantLock fairLock = new ReentrantLock(true); // 非公平锁(默认) ReentrantLock unfairLock = new ReentrantLock();公平性选择需要权衡:
- 公平锁:减少线程饥饿,但吞吐量较低
- 非公平锁:可能造成饥饿,但吞吐量高
实测数据表明,在高竞争场景下,非公平锁的性能可以比公平锁高出1-2个数量级。但在严格要求顺序性的场景(如交易系统)中,公平锁更可靠。
3.2 条件变量应用
class BoundedBuffer { final ReentrantLock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100]; int putptr, takeptr, count; 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(); } } }这个经典的有界缓冲区实现展示了如何用Condition实现精确的线程协调。我在消息队列的实现中就采用了类似模式。
4. 性能优化与最佳实践
4.1 锁分段技术
对于高并发计数器场景,可以采用锁分段优化:
class StripedCounter { private static final int N_LOCKS = 16; private final ReentrantLock[] locks = new ReentrantLock[N_LOCKS]; private final int[] counts = new int[N_LOCKS]; public StripedCounter() { for (int i = 0; i < N_LOCKS; i++) { locks[i] = new ReentrantLock(); } } public void increment() { int hash = Thread.currentThread().hashCode() % N_LOCKS; locks[hash].lock(); try { counts[hash]++; } finally { locks[hash].unlock(); } } public int getTotal() { int sum = 0; for (int i = 0; i < N_LOCKS; i++) { locks[i].lock(); try { sum += counts[i]; } finally { locks[i].unlock(); } } return sum; } }这种技术可以将竞争分散到多个锁上,我在一个百万QPS的统计系统中使用后,性能提升了8倍。
4.2 锁的注意事项
- 锁的粒度:过粗会降低并发度,过细会增加开销
- 锁的持有时间:临界区代码应尽可能短
- 死锁预防:避免嵌套锁、按固定顺序获取多个锁
- 性能监控:使用ThreadMXBean监控锁竞争情况
我在生产环境中遇到过最棘手的锁问题是:
- 锁泄漏(忘记释放)
- 锁升级(从读锁升级到写锁)
- 锁 convoy效应(大量线程排队)
5. 与synchronized的对比选择
5.1 功能对比
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 可中断 | 支持 | 不支持 |
| 超时尝试 | tryLock()支持 | 不支持 |
| 公平性 | 可配置 | 非公平 |
| 条件变量 | 多条件支持 | 单条件 |
| 性能 | JDK6+优化后相当 | 优化后相当 |
5.2 选型建议
建议使用synchronized的场景:
- 简单的同步需求
- 方法级别的同步
- 对性能要求不苛刻
建议使用ReentrantLock的场景:
- 需要可中断、超时功能
- 需要公平性控制
- 需要多个条件变量
- 需要尝试获取锁(tryLock)
我在代码审查时经常发现开发者滥用ReentrantLock的情况。实际上,在JDK6之后,经过优化的synchronized在大多数场景下性能已经与ReentrantLock相当。
6. 常见问题排查
6.1 死锁诊断
使用jstack工具可以检测死锁:
jstack <pid> | grep -A 10 "deadlock"典型死锁日志会显示线程互相等待的锁资源链。
6.2 锁竞争优化
当发现锁竞争激烈时,可以考虑:
- 减小临界区范围
- 使用读写锁(ReentrantReadWriteLock)
- 采用无锁算法(Atomic变量)
- 使用并发容器(ConcurrentHashMap)
我在处理一个缓存服务的高CPU问题时,通过将ReentrantLock替换为StampedLock,使吞吐量提升了40%。
7. 实战经验分享
最后分享几个血泪教训:
- 永远在finally块中释放锁,我曾因为异常导致锁未释放,排查了整整两天
- 避免在持有锁时调用外部方法,这可能导致死锁或性能问题
- 使用tryLock()设置合理的超时时间,我推荐100-300ms
- 对于读多写少的场景,优先考虑ReadWriteLock
- 在高并发测试时,使用JMH进行基准测试,不要依赖简单的System.currentTimeMillis()
ReentrantLock就像一把双刃剑,用好了可以大幅提升系统性能,用不好则可能引入难以排查的问题。建议在复杂同步场景中逐步应用,并配合完善的监控机制。