可重入锁ReentrantLock自 JDK 1.5 被引入,功能上与synchronized关键字类似。所谓的可重入是指,线程可对同一把锁进行重复加锁,而不会被阻塞住,这样可避免死锁的产生。ReentrantLock 的主要功能和 synchronized 关键字一致,均是用于多线程的同步。但除此之外,ReentrantLock 在功能上比 synchronized 更为丰富。比如 ReentrantLock 在加锁期间,可响应中断,可设置超时等。
ReentrantLock 是我们日常使用很频繁的一种锁,所以在使用之余,我们也应该去了解一下它的内部实现原理。ReentrantLock 内部是基于 AbstractQueuedSynchronizer(以下简称AQS)实现的。所以要想理解 ReentrantLock,应先去 AQS 相关原理。我在之前的文章 AbstractQueuedSynchronizer 原理分析 - 独占/共享模式 中,已经详细分析过 AQS 原理,有兴趣的朋友可以去看看。本文仅会在需要的时候对 AQS 相关原理进行简要说明,更详细的说明请参考我的其他文章。
2.原理本章将会简单介绍重入锁 ReentrantLock 中的一些概念和相关原理,包括可重入、公平和非公平锁等原理。在介绍这些原理前,首先我会介绍 ReentrantLock 与 synchronized 关键字的相同和不同之处。在此之后才回去介绍重入、公平和非公平等原理。
2.1 与 synchronized 的异同ReentrantLock 和 synchronized 都是用于线程的同步控制,但它们在功能上来说差别还是很大的。对比下来 ReentrantLock 功能明显要丰富的多。下面简单列举一下两者之间的差异,如下:
特性 synchronized ReentrantLock 相同可重入 是 是 ✅
响应中断 否 是 ❌
超时等待 否 是 ❌
公平锁 否 是 ❌
非公平锁 是 是 ✅
是否可尝试加锁 否 是 ❌
是否是Java内置特性 是 否 ❌
自动获取/释放锁 是 否 ❌
对异常的处理 自动释放锁 需手动释放锁 ❌
除此之外,ReentrantLock 提供了丰富的接口用于获取锁的状态,比如可以通过isLocked()查询 ReentrantLock 对象是否处于锁定状态, 也可以通过getHoldCount()获取 ReentrantLock 的加锁次数,也就是重入次数等。而 synchronized 仅支持通过Thread.holdsLock查询当前线程是否持有锁。另外,synchronized 使用的是对象或类进行加锁,而 ReentrantLock 内部是通过 AQS 中的同步队列进行加锁,这一点和 synchronized 也是不一样的。
这里列举了不少两者的相同和不同之处,暂时这能想到这些。如果还有其他的区别,欢迎补充。
2.2 可重入可重入这个概念并不难理解,本节通过一个例子简单说明一下。
现在有方法 m1 和 m2,两个方法均使用了同一把锁对方法进行同步控制,同时方法 m1 会调用 m2。线程 t 进入方法 m1 成功获得了锁,此时线程 t 要在没有释放锁的情况下,调用 m2 方法。由于 m1 和 m2 使用的是同一把可重入锁,所以线程 t 可以进入方法 m2,并再次获得锁,而不会被阻塞住。示例代码大致如下:
void m1() { lock.lock(); try { // 调用 m2,因为可重入,所以并不会被阻塞 m2(); } finally { lock.unlock() } } void m2() { lock.lock(); try { // do something } finally { lock.unlock() } }