前言:

Java中的同步类ReentrantLock是基于AbstractQueuedSynchronizer(简称为AQS)实现的。

今天从源码来了解下ReentrantLock中非公平锁的加锁和释放锁(ReentrantLock中支持公平锁和非公平锁,默认是非公平锁的,但可以通过创建ReentrantLock对象时传入参数指定使用公平锁)。

在了解ReentrantLock前,需要对AQS有一定的了解,否则在学习时会比较困难的,并且在通过源码学习ReentrantLock时也会穿插着讲解AQS内容。

AQS扫荡:

1.0、AQS中state变量

​ AQS中提供了一个int类型的state变量,并且state变量被volatile修饰,表示state变量的读写操作可以保证原子性;并且AQS还提供了针对state变量的读写方法,以及使用CAS算法更新state变量的方法。 AQS使用state变量这个状态变量来实现同步状态。

①、源码展示

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
/**
* The synchronization state.
*/
private volatile int state;

/**
* get 获取state变量值
*/
protected final int getState() {
return state;
}

/**
* set 更新state变量值
* @param newState 新的状态变量值
*/
protected final void setState(int newState) {
state = newState;
}


/**
* 使用CAS算法更新state变量值; 当从共享内存中读取出的state变量值与expect期望值一致的话,
* 就将其更新为update值。使用CAS算法保证其操作的原子性
*
* @param expect 期望值
* @param update 更新值
*/
protected final boolean compareAndSetState(int expect, int update) {
// 使用Unsafe类的本地方法来实现CAS
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

1.1、state同步状态的竞争

​ 多个线程同时竞争AQS的state同步状态,在同一时刻只能有一个线程获取到同步状态(获取到锁),那其它没获取到锁的线程该怎么办呢

它们会进去到一个同步队列中,在队列中等待同步锁的释放;

这个同步队列是一个基于链表的双向队列 , 基于链表的话,就会存在Node节点,那么AQS中节点是怎么实现的呢

①、Node节点:

AQS中自己实现了一个内部Node节点类,Node节点类中定义了一些属性,下面来简单说说属性的意思:

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
static final class Node {
// 标志在同步队列中Node节点的模式,共享模式
static final Node SHARED = new Node();
// 标志在同步队列中Node节点的模式,独占(排他)模式
static final Node EXCLUSIVE = null;

// waitStatus值为1时表示该线程节点已释放(超时等),已取消的节点不会再阻塞。
static final int CANCELLED = 1;

// waitStatus值为-1时表示当此节点的前驱结点释放锁时,然后当前节点中的线程就可以去获取锁运行
static final int SIGNAL = -1;

/**
* waitStatus为-2时,表示该线程在condition队列中阻塞(Condition有使用),
* 当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从
* 等待队列转移到同步队列中,等待获取同步锁。
*/
static final int CONDITION = -2;

/**
* waitStatus为-3时,与共享模式有关,在共享模式下,该状态表示可运行
* (CountDownLatch中有使用)。
*/
static final int PROPAGATE = -3;

/**
* waitStatus:等待状态,指的是当前Node节点中存放的线程的等待状态,
* 等待状态值就是上面的四个状态值:CANCELLED、SIGNAL、CONDITION、PROPAGATE
*/
volatile int waitStatus;

/**
* 因为同步队列是双向队列,那么每个节点都会有指向前一个节点的 prev 指针
*/
volatile Node prev;

/**
* 因为同步队列是双向队列,那么每个节点也都会有指向后一个节点的 next 指针
*/
volatile Node next;

/**
* Node节点中存放的阻塞的线程引用
*/
volatile Thread thread;

/**
* 当前节点与其next后继结点的所属模式,是SHARED共享模式,还是EXCLUSIVE独占模式,
*
* 注:比如说当前节点A是共享的,那么它的这个字段是shared,也就是说在这个等待队列中,
* A节点的后继节点也是shared。
*/
Node nextWaiter;

/**
* 获取当前节点是否为共享模式
*/
final boolean isShared() {
return nextWaiter == SHARED;
}

/**
* 获取当前节点的 prev前驱结点
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}

Node() { }

// 在后面的addWaiter方法会使用到,线程竞争state同步锁失败时,会创建Node节点存放thread
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}

Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}

②、同步队列结构图(双向队列):

1.2、图解AQS原理

​ 通过前面两点,可以了解到AQS的原理到底是什么了,总结为一句话:AQS使用一个Volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,通过CAS完成对State值的修改。

然后再来一张图,使得理解更加深刻:

图片来源: Java技术之AQS详解

好了,AQS暂时可以先了解到这里了,知道这些后,在后面了解ReentrantLock时就会变的容易些,并且后面通过源码学习ReentrantLock时,由于会使用到AQS的模版方法,所以也会讲解到AQS的内容。

剑指ReentrantLock源码:

2.0、ReentrantLock vs Synchronized

​ 在了解ReentrantLock之前,先将ReentrantLockSynchronized进行比较下,这样可以更加了解ReentrantLock的特性,也有助于下面源码的阅读;

2.1、ReentrantLock的公平锁与非公平锁

创建一个ReentrantLock对象,在创建对象时,如果不指定公平锁的话,默认是非公平锁;

①、简单了解下什么是公平锁,什么是非公平锁?

公平锁:按照申请同步锁的顺序来获取锁;

非公平锁:不会按照申请锁的顺序获取锁,存在锁的抢占;

注:后面会通过源码了解下非公平锁和公平锁是怎样获取锁的。

②、源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 默认是非公平的锁
ReentrantLock lock = new ReentrantLock();
// 构造方法默认创建了一个 NonfairSync 非公平锁对象
public ReentrantLock() {
// NonfairSync继承了Sync类,Sync类又继承了AQS类
sync = new NonfairSync();
}


// 传入参数 true,指定为公平锁
ReentrantLock lock = new ReentrantLock(true);
// 传入参数的构造方法,当fair为true时,创建一个公平锁对象,否则创建一个非公平锁对象
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

2.2、通过源码看下非公平锁的加锁机制:(独占模式)

①、开始先通过一个简单流程图来看下独占模式下加锁的流程:

​ 图片来源:美团技术团队

②、源码分析:加锁时首先使用CAS算法尝试将state状态变量设置为1,设置成功后,表示当前线程获取到了锁,然后将独占锁的拥有者设置为当前线程;如果CAS设置不成功,则进入Acquire方法进行后续处理。

1
2
3
4
5
6
7
8
9
final void lock() {
// 使用CAS算法尝试将state状态变量设置为1
if (compareAndSetState(0, 1))
// 设置成功后,表示当前线程获取到了锁,然后将独占锁的拥有者设置为当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 进行后续处理,会涉及到重入性、创建Node节点加入到队列尾等
acquire(1);
}

③、探究下acquire(1) 方法里面是什么呢 acquire(1) 方法是AQS提供的 模版方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public final void acquire(int arg) {
/**
* 使用tryAcquire()方法,让当前线程尝试获取同步锁,获取成的话,就不会执行后面的acquireQueued()
* 方法了,这是由于 && 逻辑运算符的特性决定的。
*
* 如果使用tryAcquire()方法获取同步锁失败的话,就会继续执行acquireQueued()方法,它的作用是
* 一直死循环遍历同步队列,直到使addWaiter()方法创建的节点中线程获取到锁。
*
* 如果acquireQueued()返回的true,这个true不是代表成功的获取到锁,而是代表当前线程是否存在
* 中断标志,如果存在的话,在获取到同步锁后,需要使用selfInterrupt()对当前线程进行中断。
*/
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

1)tryAcquire(arg) 方法源码解读:NonfairSync 非公平锁中重写了AQS的tryAcquire()方法

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
final boolean nonfairTryAcquire(int acquires) {
// 当前线程
final Thread current = Thread.currentThread();
// 获取当前state同步状态变量值,由于使用volatile修饰,单独的读写操作具有原子性
int c = getState();
// 如果状态值为0
if (c == 0) {
// 使用compareAndSetState方法这个CAS算法尝试将state同步状态变量设置为1 获取同步锁
if (compareAndSetState(0, acquires)) {
// 然后将独占锁的拥有者设置为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
// 如果拥有独占锁的的线程是当前线程的话,表示当前线程需要重复获取锁(重入锁)
else if (current == getExclusiveOwnerThread()) {
// 当前同步状态state变量值加1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 写入state同步状态变量值,由于使用volatile修饰,单独的读写操作具有原子性
setState(nextc);
return true;
}
return false;
}

2)addWaiter( Node.EXCLUSIVE ) :创建一个同步队列Node节点,同时绑定节点的模式为独占模式,并且将创建的节点插入到同步队列尾部;addWaiter( ) 方法是AQS提供方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private Node addWaiter(Node mode) {
// model参数是独占模式,默认为null;
Node node = new Node(Thread.currentThread(), mode);
// 将当前同步队列的tail尾节点的地址引用赋值给pre变量
Node pred = tail;
// 如果pre不为null,说明同步队列中存在节点
if (pred != null) {
// 当前节点的前驱结点指向pre尾节点
node.prev = pred;
// 使用CAS算法将当前节点设置为尾节点,使用CAS保证其原子性
if (compareAndSetTail(pred, node)) {
// 尾节点设置成功,将pre旧尾节点的后继结点指向新尾节点node
pred.next = node;
return node;
}
}
// 如果尾节点为null,表示同步队列中还没有节点,enq()方法将当前node节点插入到队列中
enq(node);
return node;
}

3)、说完addWaiter( Node.EXCLUSIVE )方法,接下来说下acquireQueued()方法,它是怎样使addWaiter()创建的节点中的线程获取到state同步锁的。(这个方法也是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
final boolean acquireQueued(final Node node, int arg) {
// 标志cancelAcquire()方法是否执行
boolean failed = true;
try {
// 标志是否中断,默认为false不中断
boolean interrupted = false;
for (;;) {
// 获取当前节点的前驱结点
final Node p = node.predecessor();
/**
* 如果当前节点的前驱结点已经是同步队列的头结点了,说明了两点内容:
* 1、其前驱结点已经获取到了同步锁了,并且锁还没释放
* 2、其前驱结点已经获取到了同步锁了,但是锁已经释放了
*
* 然后使用tryAcquire()方法去尝试获取同步锁,如果前驱结点已经释放了锁,那么就会获取成功,
* 否则同步锁获取失败,继续循环
*/
if (p == head && tryAcquire(arg)) {
// 将当前节点设置为同步队列的head头结点
setHead(node);
// 然后将当前节点的前驱结点的后继结点置为null,帮助进行垃圾回收
p.next = null; // help GC
failed = false;
// 返回中断的标志
return interrupted;
}
/**
* shouldParkAfterFailedAcquire()是对当前节点的前驱结点的状态进行判断,以及去针对各种
* 状态做出相应处理,由于文章篇幅问题,具体源码本文不做讲解;只需知道如果前驱结点p的状态为
* SIGNAL的话,就返回true。
*
* parkAndCheckInterrupt()方法会使当前线程进去waiting状态,并且查看当前线程是否被中断,
* interrupted() 同时会将中断标志清除。
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 中断标志置为true
interrupted = true;
}
} finally {
if (failed)
/**
* 如果for(;;)循环中出现异常,并且failed=false没有执行的话,cancelAcquire方法
* 就会将当前线程的状态置为 node.CANCELLED 已取消状态,并且将当前节点node移出
* 同步队列。
*/
cancelAcquire(node);
}
}

4)、最后说下 selfInterrupt() 方法, 这个方法就是将当前线程进行中断:

1
2
3
4
static void selfInterrupt() {
// 中断当前线程
Thread.currentThread().interrupt();
}

2.3、公平锁与非公平锁在加锁时的区别:

①、公平锁 FairSync 的加锁 lock() 加锁方法:

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

②、非公平锁 NonfairSync 的加锁 lock() 加锁方法:上面讲解源码的时候有提到哟,还有印象吗,没印象的话也没关系,不要哭 , 嘿嘿,我都准备好了。 源码奉上:

1
2
3
4
5
6
7
8
9
10
11
12
final void lock() {
/**
* 看到这,是不是发现了什么,非公平锁在此处直观看的话,发现比公平锁多了这几行代码;
* 这里就是使得线程存在了一个抢占,如果当前同步队列中的head头结点中 线程A 刚好释放了同步锁,
* 然后此时 线程B 正好来了,那么此时线程B就会获取到锁,而此时同步队列中head头结点的后继结点中的
* 线程C 就无法获取到同步锁,只能等待线程B释放锁后,尝试获取锁了。
*/
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

③、除了上面那处不同之外,还有别的地方吗;别急,再看看 acquire(1) 方法是否一样呢?

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

​ 诶呀,方法点进去都是一样的呀,可不嘛,都是调用的AQS提供的 acquire(1) 方法;但是别着急,上面在讲解非公平锁加锁时,有提到的 tryAcquire(arg) 方法在AQS的不同子孙类中都有各自的实现的。现在打开公平锁的 tryAcquire(arg) 方法看看其源码与非公平锁有什么区别:

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
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
/**
* 通过对比源码发现,公平锁比非公平锁多了这块代码: !hasQueuedPredecessors()
* hasQueuedPredecessors() 是做什么呢?就是判断当前同步队列中是否存在节点,如果存在节点呢,
* 就返回true,由于前面有个 !,那么就是false,再根据 && 逻辑运算符的特性,不会继续执行了;
*
* tryAcquire()方法直接返回false,后面的逻辑就和非公平锁的一致了,就是创建Node节点,并将
* 节点加入到同步队列尾; 公平锁:发现当前同步队列中存在节点,有线程在自己前面已经申请可锁,那
* 自己就得乖乖的向后面排队去。
*
* 友情提示:在生活中,我们也需要按照先来后到去排队,保证素质; 还有就是怕你们不排队被别人打了。
*/
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;
}

松口气,从中午一直写到下午快四点了,先让我歇口气,快累成狗了;本文还剩下释放锁部分没写呢,歇口气,喝口水继续

注意:ReentrantLock在释放锁的时候,并不区分公平锁和非公平锁

2.4、通过源码看下释放锁机制:(独占模式)

①、unlock() 释放锁的方法:

1
2
3
4
public void unlock() {
// 释放锁时,需要将state同步状态变量值进行减 1,传入参数 1
sync.release(1);
}

②、release( int arg ) 方法解析:(此方法是AQS提供的)

1
2
3
4
5
6
7
8
9
10
11
public final boolean release(int arg) {
// tryRelease方法:尝试释放锁,成功true,失败false
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 头结点不为空并且头结点的waitStatus不是初始化节点情况,然后唤醒此阻塞的线程
unparkSuccessor(h);
return true;
}
return false;
}

注意:这里的判断条件为什么是h != null && h.waitStatus != 0?

h == null Head还没初始化。初始情况下,head == null,第一个节点入队,Head会被初始化一个虚拟节点。所以说,这里如果还没来得及入队,就会出现head == null 的情况。

h != null && waitStatus == 0 表明后继节点对应的线程仍在运行中,不需要唤醒。

h != null && waitStatus < 0 表明后继节点可能被阻塞了,需要唤醒。

③、然后再来看看tryRelease(arg) 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected final boolean tryRelease(int releases) {
// 当前state状态值进行减一
int c = getState() - releases;
// 如果当前独占锁的拥有者不是当前线程,则抛出 非法监视器状态 异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 更新state同步状态值
setState(c);
return free;
}

④、最后看看unparkSuccessor(Node node) 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void unparkSuccessor(Node node) {
// 获取头结点waitStatus
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 获取当前节点的下一个节点
Node s = node.next;
// 如果下个节点是null或者下个节点被cancelled,就找到队列最开始的非cancelled状态的节点
if (s == null || s.waitStatus > 0) {
s = null;
// 就从尾部节点开始找,到队首,找到队列第一个waitStatus<0的节点。
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 如果当前节点的后继结点不为null,则将其节点中处于阻塞状态的线程unpark唤醒
if (s != null)
LockSupport.unpark(s.thread);
}

注意:为什么要从后往前找第一个非Cancelled的节点呢?原因如下:

由于之前加锁时的addWaiter( )方法的原因;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private Node addWaiter(Node mode) {
// model参数是独占模式,默认为null;
Node node = new Node(Thread.currentThread(), mode);
// 将当前同步队列的tail尾节点的地址引用赋值给pre变量
Node pred = tail;
// 如果pre不为null,说明同步队列中存在节点
if (pred != null) {
// 当前节点的前驱结点指向pre尾节点
node.prev = pred;
// 使用CAS算法将当前节点设置为尾节点,使用CAS保证其原子性
if (compareAndSetTail(pred, node)) {
// 尾节点设置成功,将pre旧尾节点的后继结点指向新尾节点node
pred.next = node;
return node;
}
}
// 如果尾节点为null,表示同步队列中还没有节点,enq()方法将当前node节点插入到队列中
enq(node);
return node;
}

从这里可以看到,节点入队并不是原子操作,也就是说,node.prev = pred ; compareAndSetTail( pred, node ) 这两个地方可以看作Tail入队的原子操作,但是此时 pred.next = node; 还没执行,如果这个时候执行了unparkSuccessor方法,就没办法从前往后找了,所以需要从后往前找。还有一点原因,在产生CANCELLED状态节点的时候,先断开的是Next指针,Prev指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的Node。

end! 长吸一口气,终于本文算是写完了,最后再看看有没有错别字,以及排排版。

后续还会出一篇结合CountDownLatch源码学习共享锁(共享模式)的文章。

谢谢大家阅读,鉴于本人水平有限,如有问题敬请提出。

参考资料:

1、从ReentrantLock的实现看AQS的原理及应用
2、Java技术之AQS详解