1. 可重入锁的核心概念与实现原理
可重入锁(ReentrantLock)是Java并发包中最重要的同步机制之一,它解决了传统synchronized关键字无法满足的复杂同步需求。与synchronized不同,ReentrantLock提供了更灵活的锁控制方式,这也是为什么它成为Java并发编程面试中的高频考点。
1.1 什么是可重入性
可重入性指的是同一个线程可以多次获取同一把锁而不会导致死锁。想象一下这样的场景:一个同步方法A内部调用了另一个同步方法B,这两个方法都使用同一个锁。如果锁不可重入,线程在进入方法A获取锁后,再进入方法B尝试获取同一把锁时就会被阻塞,导致死锁。
java复制public class ReentrantExample {
private final ReentrantLock lock = new ReentrantLock();
public void methodA() {
lock.lock();
try {
System.out.println("Enter method A");
methodB(); // 调用另一个需要相同锁的方法
} finally {
lock.unlock();
}
}
public void methodB() {
lock.lock();
try {
System.out.println("Enter method B");
} finally {
lock.unlock();
}
}
}
在这个例子中,如果锁不可重入,methodB将永远无法获取到锁,因为methodA已经持有它。可重入锁通过维护一个持有计数(hold count)来解决这个问题,每次锁被同一个线程获取时计数加1,释放时计数减1,只有当计数归零时锁才真正释放。
1.2 ReentrantLock的核心实现
ReentrantLock的内部实现基于AbstractQueuedSynchronizer(AQS),这是Java并发包中构建锁和同步器的框架。AQS使用一个FIFO队列来管理等待锁的线程,并通过一个volatile int类型的state变量来表示锁的状态。
java复制// ReentrantLock的简化实现原理
public class ReentrantLock implements Lock {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
// 获取锁的实现
final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 当前锁状态
if (c == 0) { // 锁未被持有
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current); // 设置当前线程为锁持有者
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 当前线程已经持有锁
int nextc = c + acquires; // 重入计数增加
setState(nextc);
return true;
}
return false;
}
// 释放锁的实现
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // 完全释放
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}
}
1.3 公平锁与非公平锁
ReentrantLock提供了两种锁获取策略:公平锁和非公平锁。这是通过构造函数的参数来指定的:
java复制ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock unfairLock = new ReentrantLock(); // 非公平锁(默认)
公平锁保证等待时间最长的线程优先获取锁,避免了线程饥饿问题,但会带来更高的性能开销。非公平锁则允许"插队",刚释放锁的线程可以立即再次尝试获取锁,减少了线程切换的开销,但可能导致某些线程长时间等待。
提示:在大多数实际场景中,非公平锁的性能要优于公平锁,因为减少了线程挂起和唤醒的开销。只有在严格要求公平性的场景下才使用公平锁。
2. ReentrantLock的高级特性与应用场景
2.1 条件变量(Condition)
ReentrantLock提供了比wait/notify更灵活的条件等待机制,通过Condition接口实现。一个锁可以关联多个Condition,这在生产者-消费者模式中特别有用。
java复制public class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition(); // 不满条件
final Condition notEmpty = lock.newCondition(); // 不空条件
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await(); // 等待不满
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal(); // 通知不空
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await(); // 等待不空
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal(); // 通知不满
return x;
} finally {
lock.unlock();
}
}
}
2.2 锁中断与超时
ReentrantLock提供了可中断的锁获取方式和带超时的锁获取方式,这是synchronized无法实现的特性。
java复制// 可中断的锁获取
public void interruptibleLock() throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
try {
lock.lockInterruptibly(); // 可被中断
try {
// 临界区代码
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
// 处理中断
}
}
// 带超时的锁获取
public void timedLock() throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
if (lock.tryLock(1, TimeUnit.SECONDS)) { // 最多等待1秒
try {
// 临界区代码
} finally {
lock.unlock();
}
} else {
// 超时处理
}
}
2.3 锁的监控与调试
ReentrantLock提供了一些监控方法,对于调试并发问题很有帮助:
java复制ReentrantLock lock = new ReentrantLock();
// ...
System.out.println("锁是否被持有: " + lock.isLocked());
System.out.println("当前线程是否持有锁: " + lock.isHeldByCurrentThread());
System.out.println("等待获取锁的线程数: " + lock.getQueueLength());
System.out.println("当前线程持有锁的次数: " + lock.getHoldCount());
3. 设计模式在并发编程中的应用
3.1 生产者-消费者模式
生产者-消费者模式是并发编程中最常用的设计模式之一,它解决了生产者和消费者速度不匹配的问题。使用ReentrantLock和Condition可以高效实现这一模式。
java复制public class ProducerConsumer {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public ProducerConsumer(int capacity) {
this.capacity = capacity;
}
public void produce(int value) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await();
}
queue.add(value);
notEmpty.signal();
} finally {
lock.unlock();
}
}
public int consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await();
}
int value = queue.remove();
notFull.signal();
return value;
} finally {
lock.unlock();
}
}
}
3.2 读写锁模式
虽然Java提供了ReentrantReadWriteLock,但我们可以用ReentrantLock和状态变量实现类似的读写锁模式:
java复制public class ReadWriteLock {
private final Lock lock = new ReentrantLock();
private final Condition noWriters = lock.newCondition();
private int readers = 0;
private boolean writer = false;
public void lockRead() throws InterruptedException {
lock.lock();
try {
while (writer) {
noWriters.await();
}
readers++;
} finally {
lock.unlock();
}
}
public void unlockRead() {
lock.lock();
try {
readers--;
if (readers == 0) {
noWriters.signalAll();
}
} finally {
lock.unlock();
}
}
public void lockWrite() throws InterruptedException {
lock.lock();
try {
while (writer || readers > 0) {
noWriters.await();
}
writer = true;
} finally {
lock.unlock();
}
}
public void unlockWrite() {
lock.lock();
try {
writer = false;
noWriters.signalAll();
} finally {
lock.unlock();
}
}
}
3.3 线程安全的单例模式
结合ReentrantLock可以实现线程安全的单例模式,相比双重检查锁定更易理解:
java复制public class Singleton {
private static Singleton instance;
private static final Lock lock = new ReentrantLock();
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
lock.lock();
try {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
} finally {
lock.unlock();
}
}
return instance;
}
}
4. ReentrantLock与synchronized的对比与选择
4.1 性能对比
在低竞争情况下,synchronized的性能与ReentrantLock相当甚至更好,因为JVM对synchronized进行了大量优化(如偏向锁、轻量级锁等)。但在高竞争情况下,ReentrantLock通常表现更好,因为它减少了线程挂起和唤醒的开销。
4.2 功能对比
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 可重入性 | 支持 | 支持 |
| 公平锁 | 不支持 | 支持 |
| 可中断锁获取 | 不支持 | 支持 |
| 超时锁获取 | 不支持 | 支持 |
| 条件变量 | 单一 | 多条件 |
| 锁状态查询 | 不支持 | 支持 |
| 锁释放 | 自动 | 手动 |
4.3 选择建议
-
优先使用synchronized的情况:
- 简单的同步需求
- 锁的获取和释放在一个方法内完成
- 不需要高级特性如条件变量、可中断锁等
-
优先使用ReentrantLock的情况:
- 需要公平锁
- 需要可中断的锁获取
- 需要带超时的锁获取
- 需要多个条件变量
- 需要跨方法获取和释放锁
- 需要监控锁的状态
注意:使用ReentrantLock时必须手动释放锁,通常在finally块中调用unlock(),否则可能导致死锁。这是比synchronized更容易出错的地方。
4.4 实际项目中的经验
在实际项目中,我发现ReentrantLock特别适合以下场景:
- 缓存更新:当多个线程可能同时触发缓存更新时,使用tryLock()可以避免重复更新:
java复制if (cacheLock.tryLock()) {
try {
if (cacheNeedsUpdate()) { // 再次检查,避免在获取锁期间已被其他线程更新
updateCache();
}
} finally {
cacheLock.unlock();
}
}
- 资源池管理:管理数据库连接池等有限资源时,使用带超时的锁获取可以避免长时间等待:
java复制if (poolLock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 分配资源
} finally {
poolLock.unlock();
}
} else {
throw new TimeoutException("获取资源超时");
}
- 复杂事务:需要跨多个方法调用的同步操作,使用ReentrantLock比synchronized更灵活:
java复制public void complexOperation() {
transactionLock.lock();
try {
step1();
step2();
step3();
} finally {
transactionLock.unlock();
}
}
在Java并发编程实践中,理解ReentrantLock的底层原理和适用场景对于设计高性能、线程安全的系统至关重要。它不仅提供了比synchronized更丰富的功能,还能帮助开发者更好地理解和控制多线程程序的执行流程。
