并发 - ReentrantLock

概述

ReentrantLock 是一个可重入锁,指的是一个线程能够对临界资源重复加锁,它是基于 AQS 的独占模式实现的同步组件,用同步状态 state 记录某个线程获取独占锁的次数,初始值为 0 ,获取锁时+1,释放锁时-1,并且它支持公平和非公平模式。

特性

说起 ReentrantLock ,就不得不提 synchronized 关键字,下面给出两者的对比关系:

特性 ReentrantLock synchronized
灵活性 支持响应中断、超时,尝试获取锁 不灵活
锁类型 公平锁或非公平锁 非公平锁
条件队列 可关联多个条件队列 关联一个条件队列
锁实现机制 依赖 AQS JVM 监视器模式
可重入性 可重入 可重入
释放锁 必须显示调用 unlock() 释放锁 自动释放

源码分析

可重入锁 ReentrantLock 是基于 AQS 实现的独占模式的同步组件,既然是基于 AQS 实现,必然遵循使用 AQS 的固定步骤,在分析源码的过程会有所体现。由于可重入锁支持公平和非公平模式,并且实现方式有所区别,因此在分析源码过程中会按照这两种情况分别说明。下面我们就通过读源码的方式来了解 ReentrantLock 是如何通过公平锁和非公平锁与 AQS 关联起来的。

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
43
44
45
46
47
48
49
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/**
* 继承 AQS 的内部类
*/
private final Sync sync;

/**
* ReentrantLock 通过在内部继承 AQS 来管理锁。真正获取锁和释放锁是由内部类 Sync 的实现类来控制的。
* 说明:
* Sync 是一个抽象的内部类,它有两个实现,分别是 NonfairSync(非公平锁)和 FairSync(公平锁)
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
// 省略其它代码
}

/**
* 默认创建非公平锁
*/
public ReentrantLock() {
sync = new NonfairSync();
}

/**
* 创建公平或非公平锁,根据传入参数决定
*
* @param fair true: 公平 false: 非公平
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

/**
* 获取锁
*/
public void lock() {
sync.lock();
}

/**
* 解锁,不区分公平还是不公平
*/
public void unlock() {
sync.release(1);
}

// 省略其它代码
}

ReentrantLock 基于 AQS 实现的独占模式的可重入锁步骤如下:

  • 定义继承自 AQS 的静态内部类 Sync
  • 由于 ReentrantLock 实现的是独占模式的锁功能,因此需要重写 tryAcquiretryRelease 方法对
  • 将 Sync 的实现组合在 ReentrantLock 的加锁(如 lock() 方法)和释放锁(如 unlock() 方法)的实现中

由于 ReentrantLock 支持公平和非公平锁,获取同步状态方法 tryAcquire 在两种实现中有所差异,因此该方法交给各自实现。静态内部类 Sync 实现了公平和非公平锁统一使用的释放同步状态的方法 tryRelease

了解了 ReentrantLock 实现独占模式的可重入锁的套路后,下面我们继续对支持锁功能的静态内部类 Sync 进行分析。

Sync

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
+--- ReentrantLock
/**
* ReentrantLock 通过在内部继承 AQS 来管理锁。真正获取锁和释放锁是由内部类 Sync 的实现类来控制的。
* 说明:
* Sync 是一个抽象的内部类,它有两个实现,分别是 NonfairSync(非公平锁)和 FairSync(公平锁)
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;

/**
* 获取锁的方法,交给 公平锁和非公平锁 两个子类实现
*/
abstract void lock();

/**
* 非公平尝试获取同步状态。
* 用于:非公平锁的 tryAcquire 和 尝试获取锁的 tryLock
*/
final boolean nonfairTryAcquire(int acquires) {
// 1 获取当前线程
final Thread current = Thread.currentThread();

// 2 获取同步状态,也就是锁资源
int c = getState();

// 3 如果 state == 0 ,说明此时没有线程持有锁
if (c == 0) {
// 直接使用 CAS 尝试获取锁。
// 注意,相比较公平锁,这里没有对同步队列进行判断,因为是非公平锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}

// 4 此时有线程持有锁,判断是否是重入的情况,根据占用锁的线程是否是当前线程
} else if (current == getExclusiveOwnerThread()) {
// 4.1 重入的情况需要操作: state = state + 1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 4.2 修改 state
setState(nextc);
return true;
}

// 5 获取锁失败
return false;
}

/**
* 可重入锁的释放锁,不区分是否为公平锁。
*
* @param releases
* @return
*/
protected final boolean tryRelease(int releases) {
// 释放同步状态
int c = getState() - releases;

// 释放锁的操作只能是获取锁的线程,否则抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();

// 标记是否完全释放,因为 ReentrantLock 支持可重入
boolean free = false;

// 如果释放后,同步状态为 0 ,说明是完全释放,那么重置独占锁的线程为 null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}

// 更新同步状态
setState(c);

// 是否完全释放,完全释放才算释放成功
return free;
}

/**
* 判断当前线程是否正在占有独占锁
*
* @return
*/
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}

/**
* 创建当前 ReentrantLock 的 Condition 对象
*
* @return
*/
final ConditionObject newCondition() {
return new ConditionObject();
}

// 省略其它方法
}

继承自 AQS 的静态内部类 Sync 主要有 5 类方法:

  1. lock 抽象方法,供子类实现,完成获取锁的逻辑
  2. nonfairTryAcquire 方法实现了非公平获取同步状态逻辑。没有定义在非公平锁实现类中是因为 ReentrantLock 中的其它方法也要使用,如 tryLock 方法
  3. tryRelease 方法实现了释放同步状态的逻辑,释放同步状态逻辑不区分是否公平
  4. isHeldExclusively 方法用于判断当前线程是否正在占有独占锁
  5. newCondition 方法用于创建当前 Lock 的 Condition 对象

非公平锁

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
+--- ReentrantLock
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;

/**
* 实现 Sync 抽象类的 lock 方法,用于非公平获取锁
*/
final void lock() {
// 1 调用 AQS 的设置通过状态的方法,尝试将同步状态由 0 设置为 1
if (compareAndSetState(0, 1))
// 1.1 将同步状态由 0 设置为 1 成功,表示获取锁成功。这里记录获取锁的线程
setExclusiveOwnerThread(Thread.currentThread());

// 2 调用 AQS 独占模式获取同步状态的方法。同步状态参数固定是 1
// acquire 方法会自动调用 tryAcquire 方法
else
acquire(1);
}

/**
* 实现 AQS 的模版方法,尝试获取独占模式的同步状态。这里是获取锁
*
* @param acquires
* @return
*/
protected final boolean tryAcquire(int acquires) {
// 调用父类 Sync 中的方法
return nonfairTryAcquire(acquires);
// 获取锁失败,回到外层调用方法:
// public final void acquire(int arg) {
// if (!tryAcquire(arg) &&
// // 线程入队及后续操作
// acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// selfInterrupt();
// }
}
}

非公平锁实现类 NonfairSync 继承了 Sync 类,实现了 Sync 中定义获取锁的 lock 方法(非公平获取锁),逻辑体现在 lock 方法和 tryAcquire 方法中;实现了AQS中定义的模版方法 tryAcquire,尝试以非公平方式获取独占模式的同步状态。

加锁

lock 方法用于非公平获取锁,逻辑如下:

  • 若通过 CAS 设置同步状态 state 成功则获取锁成功,然后将当前线程设置为独占线程即可。
  • 若通过 CAS 设置同步状态 state 失败则获取锁失败,然后进入 acquire 方法进行后续处理(先通过调用实现的 tryAcquire 方法尝试再次获取锁,失败后则进入同步队列中)

加锁完整流程图

下面对加锁过程进行说明:

1 使用方通过 ReentrantLock 暴露出去的加锁方法 lock() 进行加锁操作。
2 将加锁操作交给内部实现 NonfairSync 的加锁方法 lock() 完成。如果加锁成功则获取锁过程结束,否则执行 AQS 的 acquire 方法。
3 AQS 的 acquire 方法会执行 tryAcquire 方法,该方法由 NonfairSync 实现用来非公平尝试获取同步状态。
4 tryAcquire 是获取锁逻辑,获取失败后,会执行 AQS 框架的后续逻辑,跟 ReentrantLock 自定义同步组件无关

关于 AQS 框架的逻辑就不再展开说明,感兴趣地可以参考 AQS原理分析

公平锁

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* 公平锁实现
*/
static final class FairSync extends ReentrantLock.Sync {
private static final long serialVersionUID = -3000897897090466540L;

/**
* 实现 Sync 抽象类的 lock 方法,用于公平获取锁
*/
final void lock() {
// 调用 AQS 的模版方法,会调用实现的 tryAcquire 方法。
// 注意,这里的同步状态固定为 1
acquire(1);
}

/**
* 实现 AQS 的模版方法。公平地尝试获取锁
*
* @param acquires
* @return true - 获取锁成功(1 没有线程在等待锁 2 重入锁的情况,线程本来就持有锁,当然可以再次拿到)
*/
protected final boolean tryAcquire(int acquires) {
// 1 获取当前线程
final Thread current = Thread.currentThread();

// 2 获取同步状态,也就是锁资源
int c = getState();

// 3 如果 state == 0 ,说明此时没有线程持有锁
if (c == 0) {
// 3.1 虽然此时没有线程持有锁,但是由于这是公平锁,要讲究先来后到,此时可能在同步队列中有等待的线程节点。
// 体现公平
if (!hasQueuedPredecessors() &&

// 3.2 如果同步队列中没有线程在等待,那么就使用 CAS 尝试获取锁。不成功的情况,说明就在刚刚几乎同一时刻有其它线程抢先了
compareAndSetState(0, acquires)) {

// 3.3 获取到锁就进行标记下,告知其它线程自己持有了锁
setExclusiveOwnerThread(current);
return true;
}

// 4 此时有线程持有锁,判断是否是重入的情况,根据占用锁的线程是否是当前线程
// 体现可重入
} else if (current == getExclusiveOwnerThread()) {
// 4.1 重入的情况需要操作: state = state + 1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 4.2 修改 state
setState(nextc);
return true;
}

// 5 获取锁失败
// 回到外层调用方法:
// public final void acquire(int arg) {
// if (!tryAcquire(arg) &&
// // 线程入队及后续操作
// acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// selfInterrupt();
// }
return false;
}
}

公平锁实现类 FairSync 继承了 Sync 类,实现了 Sync 中定义获取锁的 lock 方法(公平获取锁),逻辑体现在 tryAcquire 方法中;实现了AQS中定义的模版方法 tryAcquire,尝试以公平方式获取独占模式的同步状态。

加锁

lock 方法用于公平获取锁,是直接调用 AQS 模版方法 acquire 完成的,其获取锁的逻辑封装在实现的 tryAcquired 方法中。

加锁完整流程图

下面对加锁过程进行说明:

1 使用方通过 ReentrantLock 暴露出去的加锁方法 lock() 进行加锁操作。
2 将加锁操作交给内部实现 FairSync 的加锁方法 lock() 完成,lock() 方法直接执行 AQS 的 acquire 方法。
3 AQS 的 acquire 方法会执行 tryAcquire 方法,该方法由 FairSync 实现用来公平尝试获取同步状态。
4 tryAcquire 是获取锁逻辑,获取失败后,会执行 AQS 框架的后续逻辑,跟 ReentrantLock 自定义同步组件无关

公平锁的加锁逻辑几乎和非公平锁的加锁逻辑一致,唯一不同的地方在于,在获取锁时即使没有线程持有锁,对公平锁来说要讲究先来后到,此时可能在同步队列中有等待的线程节点,此时不能抢占获取锁。

释放锁

ReentrantLock 来说,释放锁是不区分公平性的,因此释放锁的逻辑封装在了父类 Sync 中,实现如下:

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
43
44
45
46
47
48
49
+--- ReentrantLock
/**
* 解锁,不区分公平还是不公平
*/
public void unlock() {
// 同步状态固定为 1
sync.release(1);
// 释放锁后回到外层方法
// public final boolean release(int arg) {
// if (tryRelease(arg)) {
// Node h = head;
// if (h != null && h.waitStatus != 0)
// unparkSuccessor(h);
// return true;
// }
// return false;
// }
}

+--- Sync
/**
* 可重入锁的释放锁,不区分是否为公平锁。
*
* @param releases
* @return
*/
protected final boolean tryRelease(int releases) {
// 释放同步状态
int c = getState() - releases;

// 释放锁的操作只能是获取锁的线程,否则抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();

// 标记是否完全释放,因为 ReentrantLock 支持可重入
boolean free = false;

// 如果释放后,同步状态为 0 ,说明是完全释放,那么重置独占锁的线程为 null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}

// 更新同步状态
setState(c);

// 是否完全释放,完全释放才算释放成功
return free;
}

下面对 ReentrantLock 释放锁的操作进行简单概括:

  1. 使用方通过调用 ReentrantLock 的解锁方法 unlock() 进行解锁。
  2. unlock() 方法会调用内部类 Sync 的 release 模版方法,该方法是 AQS 的方法。
  3. release 中会调用 tryRelease 方法,tryRelease 需要自定义同步组件自行实现,该方法只在父类 Sync 中实现,因此可以看出,释放锁的过程并不区分是否公平。
  4. 释放锁成功后,所有处理由 AQS 框架完成,与自定义同步组件无关。

至此,可重入锁 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
43
44
45
46
47
+--- ReentrantLock
/**
* 实现 Lock 接口方法
* <p>
* 可中断获取锁
*
* @throws InterruptedException
*/
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}

/**
* 实现 Lock 接口方法
* <p>
* 尝试获取锁,获取不到也没关系,不会入队
*
* @return
*/
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}

/**
* 实现 Lock 接口方法
* <p>
* 尝试在指定的时间内获取锁
*
* @param timeout
* @param unit
* @return
* @throws InterruptedException
*/
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

/**
* 创建当前 ReentrantLock 的 Condition 对象。
* 使用 ReentrantLock 和 Condition 实现等待-通知机制
*
* @return
*/
public Condition newCondition() {
return sync.newCondition();
}

ReentrantLock 除了我们就常用的加锁方法 lock() 和解锁方法 unlock 外,还提供了上述的系列方法。值得一说的是 newCondition 方法,该方法是使用 ReentrantLockCondition 实现等待-通知机制的,具体的实现原理可参考 AQS 原理分析 - Condition实现原理

差异

公平锁和非公平锁的差异如下:

  • 非公平锁在调用 lock 后,首先会使用 CAS 尝试抢占锁,如果此时锁没有被占用,那么获取锁成功并返回。
  • 非公平锁 CAS 尝试抢占锁失败后,和公平锁一样都会进入自己实现的 tryAcquire 方法,在该方法中如果发现同步状态 state == 0 ,非公平锁会直接 CAS 抢占锁,而公平锁会判断同步队列中是否有线程节点处于等待状态,如果有则不去抢占锁,进入排队流程。

相对来说,非公平锁会有更好的性能,因为它的吞吐量更大。但非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。

小结

ReentrantLock 基于 AQS 实现了锁的机制,支持公平和非公平模式。核心思想是使用同步状态 state 控制 ReentrantLock 可重入的情况,state 初始化的时候为0,表示没有任何线程持有锁。当有线程持有该锁时,值就会在原来的基础上+1,同一个线程允许多次获得锁,此时就会多次+1,这就是可重入的概念。解锁也是对这个字段-1,一直到0,此线程释放锁。拿不到锁就借助 AQS 的同步队列管理等待线程。