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 国际许可协议 进行许可。
  目录