我们一起“扒一扒”ReentrantLock:看看锁背后那些精妙的设计
我们一起“扒一扒”ReentrantLock看看锁背后那些精妙的设计在多线程编程中锁是保障数据一致性的核心工具。而ReentrantLock作为 Java 中可重入锁的典型代表不仅功能强大其内部实现更蕴含着许多精妙的设计思想。今天我们将从实战角度出发深入剖析ReentrantLock的工作原理并通过代码示例来演示其关键特性。## 什么是 ReentrantLockReentrantLock是 Java 并发包java.util.concurrent.locks中提供的一种可重入、可中断、支持公平与非公平模式的锁。与synchronized相比它提供了更灵活的锁控制机制比如尝试获取锁、超时等待、以及条件变量Condition的支持。### 核心特性-可重入性同一个线程可以多次获取同一把锁而不会造成死锁。-公平与非公平模式非公平模式允许线程插队提高吞吐量公平模式则按请求顺序分配锁。-可中断在等待锁时可以响应中断信号。-条件变量通过Condition实现更精细的等待/通知机制。## 源码级设计基于 AQS 的锁实现ReentrantLock的核心实现依赖于AbstractQueuedSynchronizerAQS。AQS 是一个用于构建锁和同步器的框架它使用一个volatile整型变量state来表示同步状态并通过一个 FIFO 的等待队列来管理竞争线程。### 关键设计点1.状态管理state记录锁被持有的次数。初始为 0 表示未持有每次重入时state加 1。2.独占模式ReentrantLock是独占锁同一时刻只能有一个线程持有。3.Node 节点等待队列中的每个线程包装成一个Node包含线程引用、等待状态等信息。## 实战示例 1基础使用与重入验证下面是一个简单的示例演示ReentrantLock的可重入性和基本用法。javaimport java.util.concurrent.locks.ReentrantLock;public class ReentrantLockDemo { private final ReentrantLock lock new ReentrantLock(); // 默认非公平锁 private int count 0; // 模拟可重入在同一个方法中多次获取锁 public void increment() { lock.lock(); // 第一次获取锁 try { count; // 内部再次调用另一个需要锁的方法验证可重入 doSomethingElse(); } finally { lock.unlock(); // 释放锁 } } private void doSomethingElse() { lock.lock(); // 第二次获取锁可重入 try { // 执行其他操作 System.out.println(Reentrant: count count); } finally { lock.unlock(); // 释放第二次获取的锁 } } public static void main(String[] args) { ReentrantLockDemo demo new ReentrantLockDemo(); // 创建多个线程并发执行 Runnable task () - { for (int i 0; i 1000; i) { demo.increment(); } }; Thread t1 new Thread(task); Thread t2 new Thread(task); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Final count: demo.count); // 输出应为2000 }}注释解读 -lock.lock()和lock.unlock()必须成对出现且unlock通常放在finally块中确保锁被释放。 - 在increment方法中调用doSomethingElse时再次获取同一把锁说明ReentrantLock支持重入。## 公平与非公平模式性能与公平性的博弈ReentrantLock的构造函数支持传入fair参数。公平模式会严格按照线程请求锁的顺序分配锁而非公平模式则允许“插队”。### 非公平模式的优势-降低上下文切换当释放锁时如果有新线程刚好到达它可以立刻获取锁而无需唤醒等待队列中的线程。-高吞吐量在大多数场景下非公平锁的性能优于公平锁。### 公平模式的代价-线程饥饿风险低但可能因为频繁的上下文切换导致性能下降。## 实战示例 2公平与非公平锁的性能对比下面通过一个模拟银行转账的场景对比公平锁和非公平锁的表现。javaimport java.util.concurrent.locks.ReentrantLock;import java.util.concurrent.CountDownLatch;public class FairVsNonfairDemo { private static final int THREAD_COUNT 10; private static final int TRANSACTIONS_PER_THREAD 10000; private static long totalTime 0; public static void testLock(boolean fair) throws InterruptedException { final ReentrantLock lock new ReentrantLock(fair); CountDownLatch latch new CountDownLatch(THREAD_COUNT); long start System.currentTimeMillis(); for (int i 0; i THREAD_COUNT; i) { new Thread(() - { for (int j 0; j TRANSACTIONS_PER_THREAD; j) { lock.lock(); try { // 模拟转账操作简单累加实际业务中可能是账户余额更新 totalTime; // 仅用于演示实际应使用 AtomicLong } finally { lock.unlock(); } } latch.countDown(); }).start(); } latch.await(); // 等待所有线程完成 long end System.currentTimeMillis(); System.out.println((fair ? 公平锁 : 非公平锁) 耗时: (end - start) ms); } public static void main(String[] args) throws InterruptedException { // 先运行非公平锁 testLock(false); // 再运行公平锁 testLock(true); }}运行结果分析实际执行可能因环境而异 - 非公平锁通常比公平锁快 2-5 倍。这是因为公平锁需要频繁执行park/unpark操作导致上下文切换开销增大。 - 注意示例中使用totalTime并非线程安全这里仅为演示锁竞争实际场景应使用AtomicLong。## 条件变量更灵活的等待/通知机制ReentrantLock支持通过newCondition()创建多个条件变量实现了类似Object.wait/notify的功能但更精细。一个锁可以关联多个条件例如生产者-消费者模式中可以分别用notFull和notEmpty条件。### 关键设计-精准唤醒Condition.await()释放锁并等待Condition.signal()唤醒等待在该条件上的线程。-支持中断await()可以响应中断。## 总结通过上述代码示例和源码分析我们可以总结出ReentrantLock背后的精妙设计1.基于 AQS 的灵活框架通过state状态和等待队列实现了可重入、公平/非公平、可中断等特性。2.性能与公平性的平衡非公平模式通过“插队”减少线程唤醒开销适合高并发场景公平模式则适合对线程调度有严格要求的场景。3.条件变量将等待/通知机制从对象级别提升到锁级别支持多条件精细化控制避免了synchronized中notifyAll的粗放问题。在实际开发中ReentrantLock并非synchronized的替代品而是互补工具。当你需要更细粒度的控制如超时、中断、多条件时ReentrantLock是更好的选择而对于简单的互斥同步synchronized的简洁性仍然具有优势。理解其设计思想能帮助我们写出更高效、更健壮的多线程代码。