浅析JDK1.8 ReentrantLock源码。

写在开篇

ReentrantLock–重入锁,是实现Lock接口的一个同步组件。这篇文章建立在熟悉AQS源码的基础上,同时主要从两个方面来分析ReentrantLock:

  1. 重入性的实现原理
  2. 公平锁和非公平锁

类的继承关系

ReentrantLock实现了Lock和Serializable接口。

1
public class ReentrantLock implements Lock, java.io.Serializable {

成员变量

1
2
3
4
/**
* ReentrantLock通过sync(AQS的子类)来实现锁
*/
private final Sync sync;

这里再说明一下ReentrantLock语境下,AQS的成员变量:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* state用来表示该锁被线程重入的次数。
* 0表示该锁不被任何线程持有
* 1表示线程恰好持有该锁1次(未重入)
* 大于1则表示锁被线程重入state次
*/
private volatile int state;

/**
* 标识锁被哪个线程持有
*/
private transient Thread exclusiveOwnerThread;

静态内部类

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
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;

/**
* 非公平和公平锁的lock()方法有不同的实现。
*/
abstract void lock();

/**
* 非公平的独占锁获取同步状态
*/
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) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
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) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}

构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 默认构造非公平锁
*/
public ReentrantLock() {
sync = new NonfairSync();
}

/**
* @param fair true构造公平锁,false构造非公平锁
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

非公平锁

非公平模式加锁流程

加锁流程从lock.lock()开始

1
2
3
public void lock() {
sync.lock();
}

非公平锁lock的实现:

1
2
3
4
5
6
7
8
9
final void lock() {
// 尝试快速获取锁,如果state为0,更新为1
if (compareAndSetState(0, 1))
// 将当前线程标记为持有锁的线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 获取锁失败
acquire(1);
}

acquire()我们在看AQS源码就已经分析过了,再贴一下代码给大家看一下。

1
2
3
4
5
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

这里我们主要是要看ReentrantLock是怎么实现tryAcquire()的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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); // 更新state值
return true;
}
return false;
}

关于非公平锁的加锁流程,我们就看到这里,后面的步骤我们在AQS源码分析已经分析过了,无非是把当前线程包装成结点插入同步队列,通过循环检测是否能够获取到锁,如果不满足,则可能会被阻塞,直至被唤醒,其流程如下图:

ReentrantLock01
ReentrantLock01

非公平模式解锁流程

解锁从lock.unlock()开始:

1
2
3
public void unlock() {
sync.release(1);
}

release()我们在看AQS源码也分析过了,再贴一下代码给大家看一下。

1
2
3
4
5
6
7
8
9
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

我们的重点依旧放在tryRelease()中:

1
2
3
4
5
6
7
8
9
10
11
12
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 待更新的state
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // 持有锁的线程未重入
free = true;
setExclusiveOwnerThread(null); // 清除锁的持有线程标记
}
setState(c); // 更新state
return free;
}

接下来继续release()的操作。若当前线程已经完全释放锁,即锁可被其他线程使用,则还应该唤醒后续等待线程,其流程如下:

ReentrantLock02
ReentrantLock02

为什么基于FIFO的同步队列可以实现非公平锁

看到这里,可能大家还是会有疑问:由FIFO队列的特性知,先加入同步队列等待的线程会比后加入的线程更靠近队列的头部,那么它将比后者更早的被唤醒,它也就能更早的得到锁。从这个意义上,对于在同步队列中等待的线程而言,它们获得锁的顺序和加入同步队列的顺序一致,这显然是一种公平模式。那为什么说这是一种非公平的模式呢?

但线程并非只有在加入队列后才有机会获得锁,哪怕同步队列中已有线程在等待,非公平锁的不公平之处就在于此。回看下非公平锁的加锁流程,线程在进入同步队列等待之前有两次抢占锁的机会:

  1. 第一次是非重入式的获取锁,只有在当前锁未被任何线程占有(包括自身)时才能成功
  2. 第二次是在进入同步队列前,包含所有情况的获取锁的方式

只有这两次获取锁都失败后,线程才会构造结点并加入同步队列等待。而线程释放锁时是先释放锁(修改state值),然后才唤醒后继结点的线程的。试想下这种情况,线程A已经释放锁,但还没来得及唤醒后继线程C,而这时另一个线程B刚好尝试获取锁,此时锁恰好不被任何线程持有,它将成功获取锁而不用加入队列等待。线程C被唤醒尝试获取锁,而此时锁已经被线程B抢占,故而其获取失败并继续在队列中等待。

公平锁

现在我们已经知道了为什么会出现非公平锁了,那么我们就接着看一下ReentrantLock是怎么实现公平锁的吧。

从公平锁加锁的入口开始:

1
2
3
final void lock() {
acquire(1);
}

对比非公平锁,少了非重入式获取锁的方法,这是第一个不同点。

接着看tryAcquire():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
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;
}

在真正CAS获取锁之前加了判断,我们看一下hasQueuedPredecessors()

1
2
3
4
5
6
7
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

从方法名我们就可知道这是判断队列中是否有优先级更高的等待线程,队列中哪个线程优先级最高?由于头结点是当前获取锁的线程,所以队列中的第二个结点代表的线程优先级最高。

为什么非公平锁性能好

非公平锁对锁的竞争是抢占式的(队列中线程除外),线程在进入等待队列前可以进行两次尝试,这大大增加了获取锁的机会。这种好处体现在两个方面:

  1. 线程不必加入等待队列就可以获得锁,不仅免去了构造结点并加入队列的繁琐操作,同时也节省了线程阻塞唤醒的开销,线程阻塞和唤醒涉及到线程上下文的切换和操作系统的系统调用,是非常耗时的。在高并发情况下,如果线程持有锁的时间非常短,短到线程入队阻塞的过程超过线程持有并释放锁的时间开销,那么这种抢占式特性对并发性能的提升会更加明显。
  2. 减少CAS竞争。如果线程必须要加入阻塞队列才能获取锁,那入队时CAS竞争将变得异常激烈,CAS操作虽然不会导致失败线程挂起,但不断失败重试导致的对CPU的浪费也不能忽视。除此之外,加锁流程中至少有两处通过将某些特殊情况提前来减少CAS操作的竞争,增加并发情况下的性能。
    一处就是获取锁时将非重入的情况提前:
    1
    2
    3
    4
    5
    6
    final void lock() {
    if (compareAndSetState(0, 1))
    setExclusiveOwnerThread(Thread.currentThread());
    else
    acquire(1);
    }

另一处就是入队的操作,将同步队列非空的情况提前处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}

参考:https://www.cnblogs.com/takumicx/p/9402021.html