前言

ReentrantLock可重入锁功能与synchronized类似,用于协调多线程间的同步,并且提供比synchronized更为丰富的功能,比如可响应中断、锁超时等。ReentrantLock本身的实现其实较为简单,因为大部分的复杂逻辑方法已经由AQS实现了,它只需要实现少部分的关键方法即可,所以在学习ReentrantLock之前,个人认为有必要先去了解AQS。

本文将先说明重入锁的含义、公平锁与非公平锁的对比,然后进入ReentrantLock源码的分析,最后再将其与synchronized关键字进行对比。

重入锁

可重入指的是同一个线程可以对同一把锁进行重复加锁,比如线程A获取到了锁并进入了临界区,然后调用另一个同样需要该锁的方法时,它可以成功的再次获取该锁,而不会被阻塞住。那么如果锁不可重入会发生什么问题呢?很简单,还是以上面的这个例子,此时线程A再次尝试获取锁时会被阻塞,此时就发生了死锁。

ReentrantLocksynchronized关键字一样是可重入的,它的内部通过AQS的state变量记录同步状态,每当一个线程进行加锁时state++,而释放锁时state--。因此,当同一个线程重入该锁时,state就表示着该线程重入的次数。

公平与非公平

ReentrantLock是可以设置公平或非公平模式的,事实上,JDK中的许多锁实现都默认为非公平模式。在这里先简单对比一下两种模式的区别:

  • 公平锁:公平锁保障了多线程获取锁时的顺序,先到的线程先获取到锁,正常情况下每个线程都能获取到锁
  • 非公平锁:非公平锁不保障多线程获取锁时的顺序,也就是后来的线程有可能抢占了前面先来的线程获取锁的机会

公平锁保证了每个线程都能按顺序的获取到锁,而非公平锁则有可能导致前面等待许久的线程不停被后来的线程抢占,从而出现“饥饿”问题。但是从效率上来说,非公平锁会比公平锁高出许多,原因在于唤醒一个线程是需要一定时间的,此时后来的线程可以利用这段时间获取锁并执行代码逻辑,当后来的线程释放完锁后,前面的线程可能正好完全苏醒并成功获取到锁,这就有一个充分的优势:原本因为苏醒而浪费的时间被后来的线程充分利用了,而后来的线程也不会因为进入阻塞而导致线程切换的开销。因此,非公平锁的效率其实是高于公平锁的。

源码分析

了解了重入锁和公平与非公平锁后,接下来进入正式的源码分析阶段。

前面说过,ReentrantLock其实是基于AQS实现的,那么具体是怎么实现的呢?先来看看它的UML类图:

可以看出,ReentrantLock的抽象内部类Sync实现了AQS,而Sync有两个具体的子类FairSyncNonfairSync,从名字就可以看出它们分别表示公平模式和非公平模式。通过构造函数的参数可以决定选择哪种模式,如果不传入参数,则默认为非公平模式:

1
2
3
4
5
6
7
public ReentrantLock() {
sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

Sync

先看下实现了AQS的抽象内部类,相关方法在下面会介绍,这里先省略:

1
2
3
4
5
6
7
8
9
abstract static class Sync extends AbstractQueuedSynchronizer {
// 交由子类去实现,也就是 FairSync 和 NonfairSync
abstract void lock();

final boolean nonfairTryAcquire(int acquires) { // ... }

protected final boolean tryRelease(int releases) { // ... }
// ...
}

FairSync

FairSync继承自Sync,实现了公平模式的ReentrantLock的相关逻辑。

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
36
37
38
39
40
41
42
    final void lock() {
// 直接调用 AQS 的 acquire() 方法获取锁
acquire(1);
}

// 此方法在 AQS 中
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 在 AQS 中并未实现该方法,而交由这里的 FairSync 实现
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread(); // 获取当前线程
int c = getState(); // 获取同步状态
if (c == 0) { // 如果同步状态为0,表示没有任何一个线程持有锁
if (!hasQueuedPredecessors() && // 判断前面是否有等待更长时间的线程
compareAndSetState(0, acquires)) { // 如果没有,通过CAS设置同步状态
setExclusiveOwnerThread(current); // 如果设置成功了,则将当前线程设置为锁持有者
return true;
}
}
// 如果同步状态不为0,并且当前线程就是锁的持有者,那么进行锁的重入操作
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires; // 计算重入后的同步状态
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc); // 设置重入后的同步状态
return true;
}
// 获取锁失败,会执行AQS的加入同步队列的逻辑
return false;
}
}

public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t && // 如果头节点的后继节点不是当前线程,说明有等待时间更长的线程,返回true
((s = h.next) == null || s.thread != Thread.currentThread());
}

上过过程总结如下:

  1. 执行AQS的acquire()方法
  2. 调用FairSync实现的tryAcquire()方法,如果同步状态为0,则判断有没等待时间更长的线程,如果没有的话就成功获取;若同步状态不为0,且当前线程为持锁线程,则重入该锁
  3. 其它情况,一律返回false并将当前线程加入到同步队列,该过程由AQS实现

NonfairSync

NonfairSync同样继承自Sync,实现了非公平模式的ReentrantLock的相关逻辑。

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
final void lock() {
// 这里先直接CAS设置同步状态,如果设置成功,则加锁成功,不需要管同步队列前面是否有等待时间更长的线程
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
// CAS失败了,则调用此方法进入 tryAcquire() 的逻辑
else
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) {
// 与公平模式的唯一不同,不会检查前面是否有等待时间更长的线程,直接CAS
// CAS 成功就获取锁成功,失败则加入到AQS的同步队列中
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

对比公平模式的FairSync和非公平模式的NonfairSync可以发现,它们的差别其实并不大,主要体现在非公平模式在获取锁时不会先检查前面有没有其它等待的线程,而是直接野蛮式CAS,成则获取锁,败则加入同步队列。

释放锁

释放锁的逻辑比较简单,并且没有公平和非公平之分。

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
public void unlock() {
sync.release(1);
}

// 此方法在 AQS 中实现
public final boolean release(int arg) {
if (tryRelease(arg)) { // 交由子类 Sync 实现
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒后继节点
return true;
}
return false;
}

protected final boolean tryRelease(int releases) {
// 计算释放锁后的同步状态
int c = getState() - releases;
// 如果当前线程没有持有锁,调用该方法会抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // 如果释放后的同步状态为0,表示该锁完全释放了
free = true;
setExclusiveOwnerThread(null); // 将锁持有者设为 null
}
setState(c); // 设置新的同步状态
return free; // 返回该锁是否被完全释放了
}

与synchronized的异同

ReentrantLocksynchronized都是用于线程的同步控制,它们的共同点是都可重入,并且synchronized也是非公平锁(ReentrantLock默认为非公平)。而它们之间的不同主要在于以下几点:

  • ReentrantLock响应中断,而synchronized不响应
  • ReentrantLock支持超时等待,而synchronized不支持
  • ReentrantLock可设置成公平锁,而synchronized不可以
  • 发生异常时,synchronized会自动释放锁,而ReentrantLock需要手动释放锁

除此之外,ReentrantLock还提供了丰富的接口用于获取锁的状态,比如可以通过isLocked()查询ReentrantLock对象是否处于绑定状态,也可以通过getHoldCount()获取ReentrantLock的加锁次数,也就是重入次数,不过它们的本质都是调用AQS实现的getState()方法。

参考资料