Java ReentrantLock
什么是ReentrantLock?
ReentrantLock是Java 5中引入的一种可重入锁实现,它提供了与synchronized关键字类似的线程同步功能,但具有更多的灵活性和高级特性。作为java.util.concurrent.locks
包中的核心组件,ReentrantLock允许线程以独占方式访问共享资源,同时提供了公平性、可中断等额外功能。
"可重入"意味着一个线程可以多次获取同一把锁而不会导致自身死锁。例如,如果一个线程已经持有了锁,当它再次尝试获取同一把锁时,可以成功获取。
ReentrantLock vs synchronized
ReentrantLock和synchronized都能实现线程同步,但有几个关键区别:
特性 | ReentrantLock | synchronized |
---|---|---|
灵活性 | 高(显式获取和释放锁) | 低(自动获取和释放锁) |
公平性 | 支持公平锁和非公平锁 | 只支持非公平锁 |
可中断性 | 支持响应中断 | 不支持 |
超时等待 | 支持 | 不支持 |
条件变量 | 支持多个条件变量 | 只有一个条件变量 |
ReentrantLock基本用法
使用ReentrantLock的基本步骤包括:
- 创建ReentrantLock实例
- 获取锁(lock())
- 访问共享资源
- 释放锁(unlock())
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 释放锁
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo demo = new ReentrantLockDemo();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
demo.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
demo.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + demo.getCount());
}
}
输出:
Final count: 20000
始终在finally块中释放锁,以确保即使出现异常,锁也会被正确释放。否则可能导致死锁。
ReentrantLock的高级特性
1. 公平锁
通过在构造函数中设置参数,ReentrantLock可以创建公平锁:
// 默认为非公平锁
ReentrantLock unfairLock = new ReentrantLock();
// 创建公平锁
ReentrantLock fairLock = new ReentrantLock(true);
公平锁会按照线程请求的顺序获取锁,而非公平锁则允许"插队"。公平锁通常性能较低,但可以避免线程饥饿问题。
2. 可中断锁获取
ReentrantLock提供了可中断的锁获取方法:lockInterruptibly()
public void lockWithInterruption() throws InterruptedException {
lock.lockInterruptibly(); // 可中断的锁获取
try {
// 访问共享资源
} finally {
lock.unlock();
}
}
这允许线程在等待锁时能够响应中断,适用于需要取消长时间等待的场景。
3. 超时锁获取
使用tryLock()
方法可以在指定时间内尝试获取锁:
public boolean lockWithTimeout() {
try {
// 尝试在2秒内获取锁
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
// 成功获取锁,访问共享资源
return true;
} finally {
lock.unlock();
}
} else {
// 未能获取锁
return false;
}
} catch (InterruptedException e) {
// 等待过程中被中断
return false;
}
}
4. 条件变量(Condition)
ReentrantLock可以创建多个条件变量,实现更复杂的线程协调:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedBuffer {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Object[] items = new Object[100];
private int putIndex = 0, takeIndex = 0, count = 0;
// 存入元素
public void put(Object x) throws InterruptedException {
lock.lock();
try {
// 缓冲区已满,等待notFull条件
while (count == items.length) {
notFull.await();
}
// 存入元素
items[putIndex] = x;
putIndex = (putIndex + 1) % items.length;
count++;
// 通知消费者可以取出元素
notEmpty.signal();
} finally {
lock.unlock();
}
}
// 取出元素
public Object take() throws InterruptedException {
lock.lock();
try {
// 缓冲区为空,等待notEmpty条件
while (count == 0) {
notEmpty.await();
}
// 取出元素
Object x = items[takeIndex];
takeIndex = (takeIndex + 1) % items.length;
count--;
// 通知生产者可以存入元素
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
ReentrantLock工作原理
ReentrantLock的内部实现依赖于AQS(AbstractQueuedSynchronizer)框架:
当线程尝试获取锁时:
- 如果锁空闲,线程获取锁并继续执行
- 如果锁被占用,线程进入同步队列等待
- 锁被释放时,会从同步队列中唤醒一个线程
实际应用场景
场景1:高并发计数器
public class ConcurrentCounter {
private final ReentrantLock lock = new ReentrantLock();
private long count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public long getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
场景2:资源池管理
public class ResourcePool<T> {
private final ReentrantLock lock = new ReentrantLock();
private final Condition available = lock.newCondition();
private final Queue<T> pool = new LinkedList<>();
private final int maxSize;
public ResourcePool(int maxSize) {
this.maxSize = maxSize;
}
public void addResource(T resource) throws InterruptedException {
lock.lock();
try {
while (pool.size() >= maxSize) {
// 池已满,等待
available.await();
}
pool.add(resource);
available.signal(); // 唤醒等待获取资源的线程
} finally {
lock.unlock();
}
}
public T getResource() throws InterruptedException {
lock.lock();
try {
while (pool.isEmpty()) {
// 池为空,等待
available.await();
}
T resource = pool.poll();
available.signal(); // 唤醒等待添加资源的线程
return resource;
} finally {
lock.unlock();
}
}
public boolean tryGetResource(long timeout, TimeUnit unit) throws InterruptedException {
lock.lock();
try {
if (pool.isEmpty()) {
// 尝试等待指定时间
if (!available.await(timeout, unit)) {
return false; // 超时
}
}
if (!pool.isEmpty()) {
T resource = pool.poll();
available.signal();
return true;
}
return false;
} finally {
lock.unlock();
}
}
}
场景3:读写锁的简单实现
public class SimpleReadWriteLock {
private final ReentrantLock lock = new ReentrantLock();
private int readers = 0;
private boolean isWriteLocked = false;
public void lockRead() {
lock.lock();
try {
while (isWriteLocked) {
// 有写锁,等待
try {
lock.unlock();
Thread.sleep(1); // 简单实现,实际应使用条件变量
lock.lock();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
readers++;
} finally {
lock.unlock();
}
}
public void unlockRead() {
lock.lock();
try {
readers--;
} finally {
lock.unlock();
}
}
public void lockWrite() {
lock.lock();
try {
while (readers > 0 || isWriteLocked) {
// 有读锁或写锁,等待
try {
lock.unlock();
Thread.sleep(1); // 简单实现,实际应使用条件变量
lock.lock();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
isWriteLocked = true;
} finally {
lock.unlock();
}
}
public void unlockWrite() {
lock.lock();
try {
isWriteLocked = false;
} finally {
lock.unlock();
}
}
}
如果需要真正的读写锁实现,应使用Java的ReentrantReadWriteLock
类,它提供了更高效和完善的实现。
最佳实践
- 总是在finally块中释放锁:确保在所有情况下都能释放锁
- 避免在持有锁的情况下调用外部方法:可能导致死锁或性能问题
- 尽量缩短锁持有时间:提高并发性能
- 使用tryLock()处理死锁风险:提供超时机制避免无限等待
- 考虑使用公平锁处理线程饥饿问题:虽然会降低性能,但可以保证公平性
总结
ReentrantLock是Java并发编程中的重要工具,它提供了比synchronized更灵活的锁机制,包括:
- 可重入性:允许同一线程多次获取同一把锁
- 公平性选择:支持创建公平锁和非公平锁
- 可中断性:支持响应中断的锁获取操作
- 超时机制:支持带超时的锁获取尝试
- 条件变量:支持多条件等待
掌握ReentrantLock的使用,可以帮助你在复杂的并发场景中实现更精细的线程同步控制,提高应用程序的可靠性和性能。
练习题
- 使用ReentrantLock实现一个简单的计数器,支持线程安全的增加和获取操作。
- 实现一个有界阻塞队列,使用ReentrantLock和Condition实现线程安全的入队和出队操作。
- 修改有界阻塞队列,添加超时等待功能,使得入队和出队操作可以在指定时间内等待。
- 比较ReentrantLock和synchronized在不同场景下的性能差异。
进一步学习资源
- 《Java并发编程实战》(Java Concurrency in Practice)- Brian Goetz等
- Oracle官方文档:ReentrantLock
- Oracle官方教程:Lock对象