ReentrantLock与公平锁非公平锁

ReentrantLock与公平锁非公平锁

ReentrantLock 相比 synchronized 更灵活,但也更容易用错。很多人知道它支持公平锁和非公平锁,却不知道如何选择。本文重点讲使用场景、注意事项以及两者如何选择。

ReentrantLock vs synchronized

特性 ReentrantLock synchronized
锁的获取方式 显式 lock/unlock 隐式,JVM管理
公平性 支持公平/非公平 非公平
可中断 支持 不支持
超时获取 支持 不支持
条件变量 支持多个Condition 一个隐式条件
性能 JDK6+ 近似 优化后近似
代码复杂度 较高 较低

基本用法

public class Counter {
private final ReentrantLock lock = new ReentrantLock();
private int count;

public void increment() {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 必须在finally中释放
}
}
}

注意unlock() 必须在 finally 块中调用,否则异常时锁不会释放。

公平锁与非公平锁

#

创建方式

// 默认:非公平锁(性能更高)
ReentrantLock lock = new ReentrantLock();
ReentrantLock lock = new ReentrantLock(false);

// 公平锁
ReentrantLock fairLock = new ReentrantLock(true);

#

非公平锁

// 线程A持有锁
lock.lock();

// 线程B、C在队列中等待
// 线程D到来:
if (lock.tryLock()) { // 直接尝试获取,不排队
// 获取成功!插队成功
}

特点

  • 允许”插队”,后到的线程可能先获取锁
  • 吞吐量大,性能更好
  • 可能导致线程饥饿

#

公平锁

// 线程A持有锁
lock.lock();

// 线程B在队列中等待
// 线程C到来:
lock.lock(); // 检查队列,有前驱则排队

特点

  • 按请求顺序获取锁
  • 吞吐量较低
  • 避免线程饥饿

#

性能对比

public class FairnessTest {
private static final int THREAD_COUNT = 10;
private static final int ITERATIONS = 100000;

public static void main(String[] args) {
testLock(new ReentrantLock(false), "非公平锁");
testLock(new ReentrantLock(true), "公平锁");
}
}

结果:非公平锁吞吐量通常比公平锁高 5~10 倍。

建议:除非有明确的公平性需求,否则使用非公平锁。

可中断锁

public void interruptibleLock() {
try {
lock.lockInterruptibly(); // 可中断的获取
try {
// 执行业务逻辑
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
// 响应中断,取消操作
System.out.println("锁获取被中断");
}
}

使用场景:需要响应取消请求的长时任务。

超时获取

public boolean tryLockWithTimeout() {
try {
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
// 获取锁成功,执行业务
return true;
} finally {
lock.unlock();
}
} else {
// 超时未获取到锁
System.out.println("获取锁超时");
return false;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}

使用场景:避免死锁、设置操作超时时间。

Condition 条件变量

ReentrantLock 可以创建多个 Condition,实现更精细的线程等待/通知:

public class BoundedBuffer<T> {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();

private final Object[] items;
private int putIndex, takeIndex, count;

public void put(T x) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await(); // 队列满,等待
}
items[putIndex] = x;
putIndex = (putIndex + 1) % items.length;
count++;
notEmpty.signal(); // 通知消费者
} finally {
lock.unlock();
}
}

public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 队列空,等待
}
@SuppressWarnings("unchecked")
T x = (T) items[takeIndex];
takeIndex = (takeIndex + 1) % items.length;
count--;
notFull.signal(); // 通知生产者
return x;
} finally {
lock.unlock();
}
}
}

与 Object.wait/notify 对比

特性 Condition Object wait/notify
数量 多个 一个
等待队列 精确唤醒 可能唤醒错误线程
响应中断 awaitUninterruptibly 不支持
超时等待 支持纳秒级 毫秒级

锁的查询方法

ReentrantLock lock = new ReentrantLock();

// 获取当前持有锁的线程
Thread owner = lock.getOwner();

// 是否有线程在等待
boolean hasWaiters = lock.hasWaiters(condition);

// 等待队列长度
int waitQueueLength = lock.getWaitQueueLength(condition);

// 是否被当前线程持有
boolean heldByCurrent = lock.isHeldByCurrentThread();

// 是否被任意线程持有
boolean locked = lock.isLocked();

// 公平性设置
boolean fair = lock.isFair();

// 持有锁的重入次数
int holdCount = lock.getHoldCount();

读写锁 ReentrantReadWriteLock

public class Cache {
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
private Map<String, Object> data = new HashMap<>();

public Object get(String key) {
r.lock();
try {
return data.get(key);
} finally {
r.unlock();
}
}

public void put(String key, Object value) {
w.lock();
try {
data.put(key, value);
} finally {
w.unlock();
}
}
}

特点

  • 读读不互斥
  • 读写互斥
  • 写写互斥

StampedLock(JDK 8)

public class Point {
private double x, y;
private final StampedLock sl = new StampedLock();

// 乐观读
public double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
if (!sl.validate(stamp)) { // 检查是否被修改
stamp = sl.readLock(); // 升级为悲观读
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}

最佳实践

  1. 优先使用 synchronized:简单场景,JVM 优化好
  2. 需要高级功能时用 ReentrantLock:可中断、超时、公平锁、多条件
  3. 始终在 finally 中 unlock
  4. 读多写少用 ReadWriteLock
  5. 避免锁嵌套:防止死锁
// 好的模式
lock.lock();
try {
// 临界区
} finally {
lock.unlock();
}

总结

ReentrantLock 提供了比 synchronized 更灵活的锁机制,但使用复杂度也更高。在大多数场景下,synchronized 已经足够;只有在需要可中断、超时、公平性或多条件等待时,才考虑使用 ReentrantLock

核心要点

  1. ReentrantLock 支持公平锁和非公平锁,默认是非公平锁

  2. 提供了 tryLock()、lockInterruptibly() 等高级功能

  3. 必须在 finally 块中释放锁,否则可能造成死锁

  4. 公平锁性能较低,但保证线程获取锁的顺序

总结

ReentrantLock 在需要高级功能时比 synchronized 更合适。在实际项目中,大多数场景使用 synchronized 就足够了,只有在需要定时锁、可中断锁等功能时才考虑 ReentrantLock。


   转载规则


《ReentrantLock与公平锁非公平锁》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录