ReentrantLock 是 Java 并发编程中一种重要的同步机制,它比传统的 synchronized 提供了更高的灵活性和功能。下面将从 ReentrantLock 的基本原理、详细使用方法、内部实现机制、注意事项等方面详细说明。
一、ReentrantLock 的基本原理
ReentrantLock 是基于可重入的概念设计的锁。当一个线程已经获取了 ReentrantLock 锁,它可以再次进入该锁的同步代码块而不会陷入死锁。这是因为 ReentrantLock 记录了每个线程获取锁的次数,并允许同一线程多次获取它。线程获取锁的次数与释放锁的次数必须匹配,才能真正释放锁。
1.1 AQS(AbstractQueuedSynchronizer)
ReentrantLock 的核心是基于 AQS(抽象队列同步器)实现的。AQS 维护了一个队列,用于管理所有请求锁的线程。每个线程尝试获取锁时,如果当前锁被其他线程持有,则进入等待队列,阻塞等待锁释放。一旦锁释放,AQS 会从队列中唤醒下一个线程,允许它获取锁。
1.2 可重入性
可重入锁允许线程多次进入同一把锁。比如,一个递归方法可以在方法调用链中多次获取同一个锁而不导致死锁。这种特性主要依赖于线程独有的锁计数器。
1.3 公平锁与非公平锁
- 公平锁:按照线程请求锁的顺序进行排队,先到先得。公平锁避免了线程“饥饿”,但性能可能比非公平锁稍低。
- 非公平锁:线程直接尝试获取锁,不关心等待队列中的其他线程,可能会导致某些线程长期等待。非公平锁性能较高,因为它减少了上下文切换和线程调度的开销。
二、ReentrantLock 的详细使用方法
ReentrantLock 提供了比 synchronized 更多的控制能力,如尝试获取锁、可中断锁等待、锁的公平性等。下面逐一介绍其主要用法。
2.1 基本使用
在使用 ReentrantLock 时,通常遵循以下步骤:
- 创建锁对象:通过
new ReentrantLock()创建锁实例。 - 获取锁:使用
lock()或其他加锁方法获取锁。 - 执行临界区代码:在持有锁的情况下,执行需要保护的共享资源代码。
- 释放锁:使用
unlock()释放锁,确保其他线程能够继续执行。
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock(); // 创建锁
private int counter = 0; // 共享资源
public void increment() {
lock.lock(); // 获取锁
try {
counter++;
System.out.println(Thread.currentThread().getName() + " - Counter: " + counter);
} finally {
lock.unlock(); // 确保在 finally 中释放锁
}
}
public static void main(String[] args) {
ReentrantLockDemo demo = new ReentrantLockDemo();
Thread t1 = new Thread(demo::increment);
Thread t2 = new Thread(demo::increment);
t1.start();
t2.start();
}
}
2.2 尝试获取锁
使用 tryLock() 可以尝试在不阻塞的情况下获取锁。如果当前锁未被持有,则立即获取并返回 true,否则返回 false。这种方法适合用于避免死锁。
if (lock.tryLock()) {
try {
// 执行临界区代码
} finally {
lock.unlock();
}
} else {
// 锁不可用时的处理逻辑
}
还可以设置超时时间来获取锁,避免线程长时间等待锁:
try {
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 获取锁后执行的逻辑
} finally {
lock.unlock();
}
} else {
// 超时未能获取锁
}
} catch (InterruptedException e) {
e.printStackTrace();
}
2.3 可中断锁
lockInterruptibly() 允许线程在等待锁时响应中断。这对于实现中断操作的灵活性非常有用。
try {
lock.lockInterruptibly(); // 可中断的锁获取
try {
// 临界区代码
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
// 处理中断
e.printStackTrace();
}
2.4 公平锁与非公平锁
通过构造函数选择锁的公平性:
-
非公平锁(默认):
ReentrantLock lock = new ReentrantLock(); -
公平锁:
ReentrantLock lock = new ReentrantLock(true);
公平锁保证线程按顺序获取锁,但性能较低;非公平锁可能导致“饥饿”,但性能更高。
三、ReentrantLock 的内部实现
ReentrantLock 基于 AQS 实现,它通过原子操作 CAS 来控制同步状态。AQS 维护了两个核心状态:
- state:表示锁的状态,0 表示未锁定,1 表示锁定。
- 线程队列:AQS 使用一个 FIFO 队列来管理等待锁的线程。
当线程尝试获取锁时:
- 如果
state == 0,线程成功获取锁,并将state设置为 1。 - 如果锁已经被持有(
state != 0),线程进入等待队列,直到锁释放后被唤醒。
当持有锁的线程调用 unlock() 时,state 被递减。只有当 state 递减到 0 时,锁才真正释放。
四、使用 ReentrantLock 的注意事项
4.1 必须手动释放锁
ReentrantLock 和 synchronized 的主要区别之一是锁的释放必须显式调用 unlock()。忘记释放锁会导致其他线程永远无法获取锁。因此,通常在 try 块中获取锁,在 finally 块中释放锁,以确保异常情况下锁也能被正确释放。
4.2 避免死锁
当多个线程持有多个锁时,容易产生死锁问题。为了避免死锁,可以采用锁的超时机制,或者设计更合理的加锁顺序。
4.3 性能与公平性
非公平锁的性能比公平锁更好,但可能导致某些线程长期无法获得锁。公平锁避免了这种情况,但每次加锁和解锁的性能稍差。在高并发场景下,通常选择非公平锁来提升性能。
4.4 避免锁竞争
过度使用锁可能导致线程竞争和性能下降。在设计多线程应用时,尽量减少临界区的长度,避免不必要的锁。
五、总结
ReentrantLock 提供了比 synchronized 更灵活的锁控制方式。它可以:
- 选择公平锁和非公平锁;
- 提供尝试获取锁和可中断的锁;
- 允许锁的重入;
- 支持手动加锁和解锁的细粒度控制。

2万+

被折叠的 条评论
为什么被折叠?



