0%

Lock

Lock接口及其实现类:Java 并发编程的灵活锁机制

Lock 接口是 JDK 5 引入的同步工具,为 Java 并发编程提供了比 synchronized 更灵活的锁控制能力。它支持显式加锁 / 解锁、可中断获取、超时获取等特性,弥补了 synchronized 隐式操作的局限性。本文将深入解析 Lock 接口的核心实现类(ReentrantLockReentrantReadWriteLock),并对比其与 synchronized 的差异。

Lock 接口核心方法

Lock 接口定义了锁的基本操作,核心方法如下:

方法 作用描述
lock() 获取锁(阻塞式,直到获取成功)
lockInterruptibly() 可中断地获取锁(获取过程中若线程被中断,会抛出 InterruptedException
tryLock() 尝试非阻塞获取锁(成功返回 true,失败返回 false
tryLock(long timeout, TimeUnit unit) 超时获取锁(超时未获取则返回 false
unlock() 释放锁(必须在 finally 块中调用,避免锁泄漏)
newCondition() 创建条件对象,实现线程间通信(类似 synchronizedwait/notify

ReentrantLock:可重入独占锁

ReentrantLockLock 接口的最常用实现,提供与 synchronized 类似的独占锁功能,但支持更灵活的控制。其核心特性是可重入性(同一线程可多次获取锁)和公平 / 非公平模式

公平锁与非公平锁

ReentrantLock 允许通过构造函数指定锁的公平性:

1
2
3
4
// 非公平锁(默认)  
Lock lock = new ReentrantLock();
// 公平锁
Lock fairLock = new ReentrantLock(true);
(1)非公平锁(NonfairSync)
  • 获取逻辑:线程尝试直接获取锁(CAS 操作),失败后才进入等待队列;
  • 特点:可能存在线程饥饿(先请求的线程后获取锁),但吞吐量更高;
  • 适用场景:并发激烈但允许偶尔不公平的场景。
(2)公平锁(FairSync)
  • 获取逻辑:线程必须按请求顺序进入队列,只有队首线程能获取锁;
  • 特点:保证公平性,避免饥饿,但频繁的上下文切换导致吞吐量较低;
  • 适用场景:对公平性要求高的场景(如资源调度)。

核心源码解析

ReentrantLock 基于 AQS(AbstractQueuedSynchronizer)实现,内部通过 Sync 抽象类区分公平与非公平逻辑:

获取锁(lock() 方法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 非公平锁的 lock() 实现  
static final class NonfairSync extends Sync {
final void lock() {
// 尝试直接获取锁(CAS 将 state 从 0 设为 1)
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 失败则进入 AQS 的 acquire 流程
acquire(1);
}

// 尝试获取锁(可重入逻辑)
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}

// 可重入逻辑
final boolean nonfairTryAcquire(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;
if (nextc < 0) throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
释放锁(unlock() 方法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void unlock() {  
sync.release(1);
}

// AQS 的 release 方法调用 tryRelease
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;
}

多条件等待(Condition)

ReentrantLock 可通过 newCondition() 创建多个条件对象,实现更精细的线程通信:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Lock lock = new ReentrantLock();  
Condition notEmpty = lock.newCondition(); // 非空条件
Condition notFull = lock.newCondition(); // 非满条件

// 生产者线程
lock.lock();
try {
while (queue.isFull()) {
notFull.await(); // 队列满时等待
}
queue.add(item);
notEmpty.signal(); // 通知消费者
} finally {
lock.unlock();
}

// 消费者线程
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 队列空时等待
}
Object item = queue.poll();
notFull.signal(); // 通知生产者
} finally {
lock.unlock();
}

优势:相比 synchronized 的单条件队列,Condition 支持多条件分离,减少不必要的唤醒。

ReentrantReadWriteLock:读写分离锁

ReentrantReadWriteLock 是为读多写少场景优化的锁,其核心是读写分离

  • 读锁(ReadLock):共享锁,允许多个线程同时读取;
  • 写锁(WriteLock):独占锁,同一时间仅允许一个线程写入。

核心特性

  • 互斥规则:读锁与写锁互斥,写锁与写锁互斥,读锁与读锁不互斥;
  • 可重入性:读锁和写锁均支持重入(读锁重入需当前线程未持有写锁);
  • 降级与升级:写锁可降级为读锁(先获取写锁,再获取读锁,最后释放写锁),但读锁不可升级为写锁(避免死锁)。

状态设计

ReentrantReadWriteLock 复用 AQS 的 state 变量,通过位分割存储读写状态:

  • 低 16 位:写锁的重入计数(exclusiveCount);
  • 高 16 位:读锁的持有计数(sharedCount)。
1
2
3
4
5
6
7
8
static final int SHARED_SHIFT = 16;  
static final int SHARED_UNIT = (1 << SHARED_SHIFT); // 65536
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 65535

// 计算读锁计数
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 计算写锁计数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

读锁与写锁的获取与释放

读锁(ReadLock)
  • 获取:若无线程持有写锁,或持有写锁的是当前线程,则累加读锁计数;
  • 释放:递减读锁计数,直至为 0 时完全释放。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void lock() {  
sync.acquireShared(1);
}

// AQS 共享模式获取
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 若有写锁且持有者不是当前线程,获取失败
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
// 尝试获取读锁(CAS 累加高 16 位)
if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
// 记录读锁重入信息(略)
return 1;
}
// 复杂场景的重试逻辑(略)
return fullTryAcquireShared(current);
}
写锁(WriteLock)
  • 获取:若无线程持有读锁或写锁,或当前线程已持有写锁,则累加写锁计数;
  • 释放:递减写锁计数,直至为 0 时完全释放。

Lock 与 synchronized 的对比

特性 Lock(ReentrantLock) synchronized
锁获取方式 显式(lock()/unlock() 隐式(代码块 / 方法修饰)
公平性 支持公平 / 非公平模式 仅非公平模式
可中断性 支持(lockInterruptibly() 不支持(中断后仍会获取锁)
超时获取 支持(tryLock(timeout) 不支持
条件等待 多条件(Condition 单条件(对象监视器)
性能 高并发下更优(减少上下文切换) JVM 优化后与 Lock 接近
适用场景 复杂同步逻辑(超时、中断、多条件) 简单同步场景

锁的分类与最佳实践

锁的分类

  • 按可重入性:可重入锁(synchronizedReentrantLock)与不可重入锁;
  • 按公平性:公平锁与非公平锁;
  • 按共享模式:独占锁(ReentrantLock、写锁)与共享锁(读锁、Semaphore)。

最佳实践

  • 优先使用 synchronized:简单场景下代码更简洁,JVM 优化更成熟;
  • 使用 ReentrantLock:需中断、超时或多条件等待时;
  • 使用 ReentrantReadWriteLock:读多写少场景(如缓存、配置中心);
  • 避免死锁:始终在 finally 块中释放锁,按顺序获取多把锁。

欢迎关注我的其它发布渠道