
在 Java 并发编程中,除了synchronized这种原生关键字,java.util.concurrent.locks包下的Lock接口及其实现类(尤其是ReentrantLock)为开发者提供了更灵活、更强大的同步控制能力。它们弥补了synchronized的诸多局限,是构建高并发系统的重要工具。本文将从设计理念到实战应用,全面解析Lock接口与ReentrantLock的核心原理与最佳实践。
Lock接口是 Java 5 引入的同步机制规范,它将锁的获取与释放等操作抽象为显式方法,打破了synchronized关键字的语法束缚,为并发控制带来了前所未有的灵活性。
Lock接口定义了锁操作的基本规范,核心方法包括:
这些方法的设计体现了Lock的核心优势:显式控制、可中断、超时获取、多条件等待。
特性 | synchronized | Lock(以 ReentrantLock 为例) |
|---|---|---|
获取与释放 | 隐式(编译器自动处理) | 显式(需手动调用 lock () 和 unlock ()) |
可中断性 | 不可中断 | 可通过 lockInterruptibly () 中断 |
超时机制 | 无 | 支持 tryLock (time, unit) 超时获取 |
公平性 | 非公平(无法设置) | 可通过构造函数指定公平 / 非公平 |
条件等待 | 依赖 Object 的 wait ()/notify () | 支持多个 Condition 对象,更灵活 |
锁状态查询 | 无法直接查询 | 可通过 isHeldByCurrentThread () 等方法查询 |
性能 | 低竞争下与 Lock 接近,高竞争略差 | 高竞争下性能更稳定 |
synchronized就像一辆自动挡汽车,简单易用但功能有限;Lock则像手动挡,操作复杂但能应对更多场景。
ReentrantLock是Lock接口最常用的实现类,其名称中的 “Reentrant” 表示可重入性—— 即线程可以重复获取同一把锁,这与synchronized的特性一致,但在功能上更加强大。
可重入性指一个线程已经获取锁后,再次获取该锁时不会被阻塞。这一特性通过计数器实现:
public class ReentrantDemo { private static final Lock lock = new ReentrantLock(); public static void main(String[] args) { lock.lock(); try { System.out.println("第一次获取锁"); // 再次获取同一把锁(可重入) lock.lock(); try { System.out.println("第二次获取锁"); } finally { lock.unlock(); // 释放第二次获取的锁 } } finally { lock.unlock(); // 释放第一次获取的锁 } }}上述代码中,线程两次获取同一ReentrantLock,不会发生死锁,体现了可重入性。
ReentrantLock通过构造函数支持两种锁模式:
// 非公平锁(默认)Lock nonFairLock = new ReentrantLock();// 公平锁(需显式指定)Lock fairLock = new ReentrantLock(true);公平锁的实现代价:需要维护一个有序队列记录等待线程,每次获取锁时都要检查队列,增加了额外开销。因此,非公平锁是大多数场景的首选。
synchronized通过Object的wait()、notify()实现线程间协作,但存在局限性(如一个锁只能关联一个等待队列)。ReentrantLock的newCondition()方法可创建多个Condition对象,实现更精细的线程协作。
Condition接口的核心方法:
典型场景:生产者 - 消费者模型中,用两个Condition分别处理 “队列满” 和 “队列空” 的等待:
public class ConditionDemo { private final Lock lock = new ReentrantLock(); // 队列满时的等待条件 private final Condition notFull = lock.newCondition(); // 队列空时的等待条件 private final Condition notEmpty = lock.newCondition(); private final Queue<Integer> queue = new LinkedList<>(); private static final int MAX_SIZE = 10; public void produce(int value) throws InterruptedException { lock.lock(); try { // 队列满则等待 while (queue.size() == MAX_SIZE) { notFull.await(); // 释放锁,进入notFull等待队列 } queue.add(value); notEmpty.signal(); // 唤醒等待队列空的线程 } finally { lock.unlock(); } } public int consume() throws InterruptedException { lock.lock(); try { // 队列空则等待 while (queue.isEmpty()) { notEmpty.await(); // 释放锁,进入notEmpty等待队列 } int value = queue.poll(); notFull.signal(); // 唤醒等待队列满的线程 return value; } finally { lock.unlock(); } }}相比synchronized的单条件等待,Condition的多条件分离使代码逻辑更清晰,避免了不必要的唤醒(如只唤醒需要的生产者或消费者)。
ReentrantLock的lockInterruptibly()和带超时的tryLock()方法,为处理死锁等问题提供了更多手段。
当线程长时间获取不到锁时,可通过中断机制使其退出等待,避免无限阻塞:
public class InterruptibleLockDemo { private static final Lock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { try { // 可中断地获取锁 lock.lockInterruptibly(); try { Thread.sleep(10000); // 模拟长时间操作 } finally { lock.unlock(); } } catch (InterruptedException e) { System.out.println("线程1被中断,放弃获取锁"); } }); lock.lock(); // 主线程先获取锁 t1.start(); Thread.sleep(1000); t1.interrupt(); // 中断线程1的等待 lock.unlock(); }}线程 1 在获取锁时被中断,会抛出InterruptedException并终止,避免了永久阻塞。
通过tryLock(time, unit)可设置获取锁的超时时间,超时后线程可选择其他处理逻辑:
public class TimeoutLockDemo { private static final Lock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { try { // 尝试在2秒内获取锁 if (lock.tryLock(2, TimeUnit.SECONDS)) { try { System.out.println("线程1获取到锁"); Thread.sleep(3000); } finally { lock.unlock(); } } else { System.out.println("线程1超时未获取到锁"); } } catch (InterruptedException e) { e.printStackTrace(); } }); lock.lock(); t1.start(); Thread.sleep(3000); // 主线程持有锁3秒 lock.unlock(); }}线程 1 的超时时间为 2 秒,而主线程持有锁 3 秒,因此线程 1 会因超时而放弃获取锁。
ReentrantLock的强大功能源于其基于AbstractQueuedSynchronizer(AQS) 框架的实现。AQS 是 Java 并发工具的基础,通过同步状态和等待队列实现锁的获取与释放。
ReentrantLock的释放需要手动调用unlock(),若忘记释放或在获取锁后抛出异常,会导致锁永久持有,引发死锁。正确写法:
Lock lock = new ReentrantLock();lock.lock();try { // 临界区操作} finally { lock.unlock(); // 确保释放锁}虽然Condition提供了灵活的等待机制,但过多的条件变量会增加代码复杂度。简单场景下,synchronized的wait()/notify()可能更简洁。
ReentrantLock作为Lock接口的代表实现,通过显式控制、可中断、超时机制、多条件等待等特性,为 Java 并发编程提供了远超synchronized的灵活性。其基于 AQS 框架的实现,既保证了线程安全,又兼顾了性能。
但强大的功能也意味着更高的使用门槛:必须手动释放锁、需处理中断和异常、公平性选择需谨慎。开发者需深入理解其原理,才能在实际场景中扬长避短。
无论是synchronized还是ReentrantLock,都不是 “银弹”。在并发编程中,没有万能的工具,只有适合的选择。理解不同锁机制的底层逻辑,根据场景灵活运用,才能构建高效、安全的并发系统。
最后记住:锁是用来解决问题的,而非制造问题的。合理使用ReentrantLock,让它成为并发编程的助力,而非负担。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。