Java并发-重入锁ReentrantLock详解与实践
在Java并发编程中,除了使用synchronized
关键字外,JDK还提供了更加灵活强大的锁机制。ReentrantLock
(重入锁)作为显式锁的典型代表,不仅能够实现与synchronized
相同的互斥和内存可见性保证,还提供了更丰富的功能特性。
# 一、什么是ReentrantLock
ReentrantLock
是Java并发包(java.util.concurrent.locks
)中提供的可重入互斥锁。之所以叫"重入锁",是因为同一个线程可以多次获取同一把锁而不会发生死锁。相比synchronized
,它提供了更细粒度的锁控制和更丰富的功能。
# 二、基础使用方式
让我们从一个简单的例子开始了解ReentrantLock
的基本用法:
public class ReentrantLockDemo implements Runnable {
private static final ReentrantLock lock = new ReentrantLock();
private static int counter = 0;
@Override
public void run() {
for (int i = 0; i < 1000000; i++) {
lock.lock();
try {
counter++;
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo demo = new ReentrantLockDemo();
Thread t1 = new Thread(demo);
Thread t2 = new Thread(demo);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终结果:" + counter);
}
}
这个例子展示了ReentrantLock
的核心用法模式:通过lock()
方法获取锁,在finally
块中通过unlock()
方法释放锁。这种显式的加锁和解锁方式给了我们更多的控制权。
性能对比:在早期的Java版本中(Java 5),
ReentrantLock
的性能明显优于synchronized
。但从Java 6开始,synchronized
经过大量优化后,两者的性能已经非常接近。
# 三、中断响应能力
synchronized
关键字的一个局限性在于,当线程在等待锁时无法响应中断。而ReentrantLock
提供了lockInterruptibly()
方法来解决这个问题:
public class InterruptibleLockDemo implements Runnable {
private static final ReentrantLock lock1 = new ReentrantLock();
private static final ReentrantLock lock2 = new ReentrantLock();
private final int lockType;
public InterruptibleLockDemo(int lockType) {
this.lockType = lockType;
}
@Override
public void run() {
try {
if (lockType == 1) {
// 线程1:先获取lock1,再获取lock2
lock1.lockInterruptibly();
try {
Thread.sleep(500);
lock2.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + " 获取到两把锁");
} finally {
lock2.unlock();
}
} finally {
lock1.unlock();
}
} else {
// 线程2:先获取lock2,再获取lock1
lock2.lockInterruptibly();
try {
Thread.sleep(500);
lock1.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + " 获取到两把锁");
} finally {
lock1.unlock();
}
} finally {
lock2.unlock();
}
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 被中断");
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new InterruptibleLockDemo(1), "线程1");
Thread t2 = new Thread(new InterruptibleLockDemo(2), "线程2");
t1.start();
t2.start();
Thread.sleep(1000);
// 中断线程2,打破死锁
t2.interrupt();
}
}
当线程在等待获取锁的过程中被中断时,lockInterruptibly()
会抛出InterruptedException
,从而可以优雅地处理中断情况。
# 四、超时机制
有时候我们希望在等待锁的时候设置一个超时时间,避免无限期等待。ReentrantLock
提供了tryLock()
方法来实现这一功能:
public class TimeoutLockDemo implements Runnable {
private static final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " 获取到锁,开始工作");
Thread.sleep(3000); // 模拟工作时间
System.out.println(Thread.currentThread().getName() + " 工作完成");
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " 获取锁超时,放弃执行");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 被中断");
}
}
public static void main(String[] args) throws InterruptedException {
TimeoutLockDemo demo = new TimeoutLockDemo();
Thread t1 = new Thread(demo, "线程1");
Thread t2 = new Thread(demo, "线程2");
t1.start();
Thread.sleep(100); // 确保t1先启动
t2.start();
t1.join();
t2.join();
}
}
tryLock(long time, TimeUnit unit)
方法允许线程在指定时间内尝试获取锁,超时后会放弃等待。这种机制在高并发场景下非常有用,可以避免线程长时间阻塞。
# 五、公平锁与非公平锁
默认情况下,ReentrantLock
是非公平锁,即不保证等待时间最长的线程优先获取锁。这种策略虽然可能导致某些线程饥饿,但整体吞吐量更高。
public class FairLockDemo implements Runnable {
// 创建公平锁
private static final ReentrantLock fairLock = new ReentrantLock(true);
// 创建非公平锁
private static final ReentrantLock unfairLock = new ReentrantLock(false);
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 启动");
for (int i = 0; i < 5; i++) {
fairLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获得公平锁");
} finally {
fairLock.unlock();
}
}
}
public static void main(String[] args) {
FairLockDemo demo = new FairLockDemo();
// 启动多个线程测试公平锁
for (int i = 0; i < 5; i++) {
new Thread(demo, "线程" + i).start();
}
}
}
公平锁保证了按照请求锁的顺序获取锁,避免了饥饿现象,但由于需要维护队列,性能会有所下降。在实际应用中,需要根据具体场景选择合适的锁类型。
# 六、Condition:锁的得力助手
Condition
接口提供了类似Object.wait()
和Object.notify()
的功能,但更加灵活。一个ReentrantLock
可以创建多个Condition
对象,实现更精确的线程通信。
public class ConditionDemo {
private static final ReentrantLock lock = new ReentrantLock();
private static final Condition condition = lock.newCondition();
private static boolean ready = false;
static class Worker implements Runnable {
@Override
public void run() {
lock.lock();
try {
while (!ready) {
System.out.println(Thread.currentThread().getName() + " 等待条件满足");
condition.await();
}
System.out.println(Thread.currentThread().getName() + " 条件满足,开始工作");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
// 启动工作线程
Thread worker1 = new Thread(new Worker(), "工作线程1");
Thread worker2 = new Thread(new Worker(), "工作线程2");
worker1.start();
worker2.start();
Thread.sleep(2000);
// 唤醒等待的线程
lock.lock();
try {
ready = true;
condition.signalAll();
System.out.println("主线程通知条件已满足");
} finally {
lock.unlock();
}
}
}
Condition
的主要方法包括:
await()
:当前线程等待,直到被唤醒或中断awaitUninterruptibly()
:当前线程等待,不响应中断signal()
:唤醒一个等待线程signalAll()
:唤醒所有等待线程
需要注意的是,调用Condition
的方法前必须先获取对应的锁,否则会抛出IllegalMonitorStateException
异常。
# 七、实际应用:ArrayBlockingQueue
JDK中的ArrayBlockingQueue
就是ReentrantLock
和Condition
配合使用的经典案例:
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
/** 保护所有访问的主锁 */
final ReentrantLock lock;
/** 等待取元素的条件 */
private final Condition notEmpty;
/** 等待放元素的条件 */
private final Condition notFull;
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await(); // 队列为空时等待
return dequeue();
} finally {
lock.unlock();
}
}
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await(); // 队列满时等待
enqueue(e);
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal(); // 通知等待取元素的线程
}
}
在这个实现中,ArrayBlockingQueue
使用一个ReentrantLock
和两个Condition
对象:
notEmpty
:当队列不为空时的条件notFull
:当队列不满时的条件
当队列为空时,take()
操作会在notEmpty
条件上等待;当队列满时,put()
操作会在notFull
条件上等待。通过这种方式实现了高效的生产者-消费者模式。
# 八、总结
ReentrantLock
作为Java并发编程的重要工具,提供了比synchronized
更丰富的功能:
- 可中断的锁获取:通过
lockInterruptibly()
方法实现 - 超时机制:通过
tryLock()
方法实现 - 公平锁支持:可以选择公平或非公平策略
- 多条件支持:通过
Condition
实现更精确的线程通信 - 锁状态查询:提供了丰富的锁状态查询方法
在选择锁机制时,如果只需要基本的互斥功能,synchronized
依然是首选,因为它使用简单且JVM层面优化充分。但如果需要更高级的功能,如可中断、超时、公平性或者条件变量,那么ReentrantLock
就是不二之选。
扩展阅读:深入理解AbstractQueuedSynchronizer (opens new window),了解ReentrantLock
底层实现原理。