什么是可重入锁
概念
可重入锁(Reentrant Lock)是一种锁机制,允许同一个线程在持有锁的情况下再次获得该锁,而不会被阻塞或导致死锁。这种锁的主要特点是支持锁的递归获取,即如果一个线程已经持有了锁,那么它可以在持有锁的过程中再次获得该锁。
可重入锁的关键特点
- 递归性:当一个线程获得锁之后,它可以再次进入同步代码块或方法而不会被阻塞。这对于递归方法或需要多次进入相同同步代码的复杂场景非常有用。
- 锁计数:可重入锁通常会维护一个计数器来记录锁的持有次数。每当同一线程获取锁时,计数器增加;当线程释放锁时,计数器减少。只有当计数器减到0时,锁才会真正被释放。
- 避免死锁:由于同一个线程可以多次获取锁,因此在递归调用或复杂的调用链中不会因为尝试重新获取锁而导致死锁。
可重入锁的实现
在Java中,synchronized
和 ReentrantLock
都是可重入锁的实现:
synchronized
关键字:Java的内置锁是可重入的。如果一个线程进入一个被synchronized
修饰的方法或代码块,并试图再次进入该锁定的同步方法或代码块,它不会被阻塞。ReentrantLock
类:这是Java并发包中提供的一种显式锁,它也支持可重入性。与synchronized
相比,ReentrantLock
提供了更多的功能,如可中断锁请求、定时锁请求和条件变量支持。
示例代码
synchronized
的可重入性:
public class ReentrantExample {
public synchronized void method1() {
System.out.println("Inside method1");
method2(); // 同一线程可以再次获取锁并进入 method2
}
public synchronized void method2() {
System.out.println("Inside method2");
}
public static void main(String[] args) {
ReentrantExample example = new ReentrantExample();
example.method1(); // 将同时调用 method1 和 method2
}
}
ReentrantLock
的可重入性:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void method1() {
lock.lock();
try {
System.out.println("Inside method1");
method2(); // 同一线程可以再次获取锁并进入 method2
} finally {
lock.unlock();
}
}
public void method2() {
lock.lock();
try {
System.out.println("Inside method2");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
example.method1(); // 将同时调用 method1 和 method2
}
}
ReentrantLock
和 synchronized
的对比
ReentrantLock
和 synchronized
都是用于实现线程同步的锁机制,但它们有一些重要的区别。以下是它们的主要区别:
1. 锁的获取和释放方式
-
synchronized
- 是Java中的一种隐式锁。
- 锁的获取和释放是隐式的,开发者不需要手动操作。当一个线程进入一个
synchronized
方法或代码块时,锁会自动被获取;当线程退出方法或代码块时,锁会自动释放。
-
ReentrantLock
- 是一种显式锁。
- 锁的获取和释放需要显式地调用
lock()
和unlock()
方法。开发者必须确保每次获取锁后都要在finally
块中释放锁,以避免死锁。
2. 功能和灵活性
synchronized
:- 功能简单,适用于基本的同步需求。
- 不支持可中断锁定、尝试锁定(即在超时时间内获取锁)或公平锁等高级功能。
ReentrantLock
:- 提供了更多高级功能和更大的灵活性。
- 支持以下高级功能:
- 可中断锁定:可以使用
lockInterruptibly()
方法尝试获取锁,支持响应中断。 - 尝试锁定:可以使用
tryLock()
方法尝试获取锁,失败后不阻塞。 - 定时锁定:可以使用
tryLock(long timeout, TimeUnit unit)
方法在指定时间内尝试获取锁。 - 公平锁:可以通过构造函数
ReentrantLock(boolean fair)
来创建一个公平锁,确保线程按照先后顺序获取锁。 - 条件变量:通过
newCondition()
方法获取Condition
对象,可以实现比synchronized
更复杂的线程间通信。
- 可中断锁定:可以使用
3. 性能
synchronized
- 在Java早期版本中,
synchronized
的性能较差。 - 从Java 6开始,JVM对
synchronized
进行了很多优化,如偏向锁、自旋锁等,使得它在许多情况下性能已经非常接近ReentrantLock
。
- 在Java早期版本中,
ReentrantLock
- 在高竞争环境下通常表现得更好,特别是在需要频繁尝试锁定或使用可中断锁的场景中。
4. 可中断性
synchronized
: 不支持中断。当一个线程在等待锁时,不能被中断。ReentrantLock
: 支持中断,使用lockInterruptibly()
方法可以在等待锁时响应中断。
5. 条件变量(Condition)
synchronized
- 只能通过对象的
wait()
、notify()
和notifyAll()
方法来实现线程间的等待/通知机制,且只能有一个等待队列。
- 只能通过对象的
ReentrantLock
- 提供了更强大的条件变量支持,可以通过
newCondition()
方法创建多个Condition
对象,形成多个等待队列,实现更加灵活的线程间通信。
- 提供了更强大的条件变量支持,可以通过
6. 可重入性
- 两者都支持:
ReentrantLock
和synchronized
都是可重入的,意味着一个线程可以多次获取同一把锁而不会被阻塞。
7. 公平性
synchronized
:- 默认是非公平的,锁的分配可能会造成某些线程的“饥饿”状态。
ReentrantLock
:- 可以通过构造方法指定是否使用公平锁。
选择建议
- 简单场景:如果只是需要简单的同步机制,
synchronized
通常是首选,代码更简洁,且无需担心锁的释放问题。 - 复杂场景:如果需要更多控制和高级功能,比如可中断锁、尝试锁、多个条件变量或公平锁,
ReentrantLock
是更合适的选择。