RenntrantLock中lock过程解析

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
new Thread(() -> {
try {
lock.lock();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
}

公平锁与非公平锁初识

直接点进lock()方法

1
2
3
public void lock() {
sync.lock(); // lock方法为Sync类中的抽象方法,具体交由它的子类(FairSync和NonfairSync)来实现
}

直接调用sync的lock方法,Sync是RenntrantLock的一个内部类,Sync这个lock方法是抽象方法,实则分别调用的是它的两个子类的lock方法。

FairSync

1
2
3
final void lock() {
acquire(1); // 公平锁直接调用AQS中的acquire(int)方法
}

NonfairSync

1
2
3
4
5
6
final void lock() { // 非公平锁,先尝试获取锁,这里和公平锁的区别可以看到了吧,公平锁直接执行else中代码!!!所以这个效率比公平锁高
if (compareAndSetState(0, 1)) // CAS尝试获取锁
setExclusiveOwnerThread(Thread.currentThread()); // 获取成功将当前线程设置为成员变量(exclusiveOwnerThread)拥有锁的线程
else
acquire(1); // 否则就和公平锁一样使用AQS中的acquire(int)方法来进行后续处理。
}

代码中的注释写出了非公平锁和公平锁的区别,好比超市排队结账,非公平锁就是买完东西后一来就直接要插队,要去结账(插队操作就是CAS操作),然后发现此时收银员正在给另外一个人结账(也就是锁被其他线程占用),那就乖乖排队,执行acquire(1);公平锁就好比一个老实人,乖乖排队直接acquire(1)。

acquire方法

它们都要进入acquire方法,这个方法是AQS类中的一个方法。

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

它们都会执行tryAcquire(arg)这个方法,这个方法的意思是尝试获取锁,但是这个方法在AQS中并未具体实现,子类来实现这个方法,模板方法设计模式的一种体现。

tryAcquire方法

FairSync中

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) { // 获取锁成功true,否则false
final Thread current = Thread.currentThread(); // 获取当前线程
int c = getState(); // 获取锁当前状态
if (c == 0) { // 如果当前状态为0则表示锁未被占用
if (!hasQueuedPredecessors() && // 判断自己是否需要排队
compareAndSetState(0, acquires)) { // CAS设置这个状态为acquires
setExclusiveOwnerThread(current); // 设置当前线程为拿到锁的这个线程
return true; // 返回true
}
}
else if (current == getExclusiveOwnerThread()) { // state不是0,则说明锁被持有,判断一下当前请求线程是否等于持有这个锁的线程。这里体现了ReentrantLock的重入性!!!
int nextc = c + acquires; // 原来的状态加上acquires
if (nextc < 0) // 判断一下是否小于0,基本不可能出现这个情况
throw new Error("Maximum lock count exceeded");
setState(nextc); // 重新对state进行赋值
return true; // 返回true
}
return false; // 获取失败返回false,锁在被其它线程占用并且占用这个锁的线程也不是当前请求的线程。
}

NonfairSync中

1
protected final boolean tryAcquire(int acquires) {    return nonfairTryAcquire(acquires);}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) { // 这里可以看出非公平锁没有调用!hasQueuedPredecessors()这个方法,也就是无需判断自己是否排队。其它操作和公平锁相同。
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;
}

小结

明白上述操作就应该明白了

  1. 非公平锁和公平锁的区别?
  2. RenntrantLock为什么是可重入锁?
  3. RenntrantLock类的内部结构?

之后读源码就该按照一条线来分析了,上述操作是按照两条线,(公平锁和非公平锁),下面就来具体分析公平锁的加锁是怎么实现的,因为公平锁比非公平锁多一点东西,公平锁弄懂,非公平锁自然也就容易明白了。

详解公平锁加锁过程

前面调用了FairSync中的tryAcquire方法后,如果获取成功返回true,否则返回false。

1
2
3
4
5
public final void acquire(int arg) {
if (!tryAcquire(arg) && //这里假如FairSync的这个方法返回的是false获取锁失败,要是返回true的话就不用往下看了。
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

分析acquireQueued(addWaiter(Node.EXCLUSIVE), arg)这个方法,先看其中的addWaiter这个方法,实际上这里你想一下,自己获取锁失败了,结果就是自己要去排队了,就是实际处理更加复杂而已。

addWaiter方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private Node addWaiter(Node mode) { // 这个方法就是为当前线程排队节点,并且初始化这个队列,这个参数的意思就是你是共享模式(Node.SHARED)还是独占模式(Node.EXCLUSIVE)。
Node node = new Node(Thread.currentThread(), mode); // 封装当前线程为Node结点
// 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)) { // CAS设置当前线程封装的结点为尾结点,之所以CAS操作是因为这时候有可能会有其它线程更改了这个尾结点!!!
pred.next = node; // CAS设置成功 将原来的尾结点的下一个指针设置为当前线程结点
return node; // 返回当前线程的结点
}
}
enq(node); // 当前队列未初始化或者CAS设置失败(在这个过程中别的线程将尾结点更改)
return node; // 返回当前线程的结点
}

我感觉这个实际上说初始化可能不太准确,先看Node结点的成员变量组成都有哪些。(注意并未列出全部只是列出部分当前使用到的成员变量)

1
2
3
4
5
6
7
8
9
10
// 结点共享模式下的标记
static final Node SHARED = new Node();
// 结点独占模式下的标记
static final Node EXCLUSIVE = null;
// 上一个结点
volatile Node prev;
// 下一个结点
volatile Node next;
// 这个结点的线程
volatile Thread thread;

这是这个Node类中目前使用到的部分成员变量,从这些元素中就可以看出这个是一个双向链表的一个结点。然后分析AQS中的一些成员变量。

1
2
3
4
5
6
// 头结点
private transient volatile Node head;
// 尾结点
private transient volatile Node tail;
// 锁的状态
private volatile int state;

知道这些后肯定就认识到了,在这个队列未初始化之前的情况是:

enq方法

继续向下执行,上述addWaiter假如我们未初始化,或者CAS失败,然后会执行enq这个方法。这个方法主要难度在理解这个for死循环,构成了不断自旋操作,使用CAS+自旋设置tail结点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Node enq(final Node node) {
for (;;) { // 死循环
Node t = tail; // 获得当前尾结点
if (t == null) { // 尾结点为空初始化这个结点
if (compareAndSetHead(new Node())) // new 一个Node结点,CAS设置为头结点
tail = head; // 让尾结点也指向这个结点
} else { // 已经初始化
node.prev = t; // node结点的上一个指向尾结点(这个node结点是你本线程构造出来的一个结点)
if (compareAndSetTail(t, node)) { // CAS设置尾结点为你当前线程的结点
t.next = node; // 设置成功将以前尾结点的下一个结点指针指向当前线程的结点
return t; // 返回旧的尾结点
}
}
}
}

这个操作执行完后那么这个addWaiter方法就彻底执行完毕了,此刻的链表的情况为:

小结

这个图应该很清楚了吧,那个thread那个t1是我随便写的,表示的线程对象。

现在应该明白了addWaiter()这个方法是做什么的了吧!!!

acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法

前面已经了解了addWaiter这个方法,这个方法返回的是当前线程封装后的Node结点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { // 死循环
final Node p = node.predecessor(); // 获取当前线程结点的前一个结点!
if (p == head && tryAcquire(arg)) { // 如果这当前线程的前一个结点为头结点那么就执行tryAcquire方法尝试获取锁,这是自旋的一种体现
setHead(node); // 获取锁成功,将当前线程节点设置为头结点
p.next = null; // 断绝之前的头结点和现在的头结点直线的关系,方便将其GC回收
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && // 当前节点不是线程中的第二个节点(也就是你前一个节点是head节点)或者tryAcquire获取锁失败,执行此方法
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

现在来看shouldParkAfterFailedAcquire这个方法

shouldParkAfterFailedAcquire方法

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
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 获取你前一个结点的一个状态
if (ws == Node.SIGNAL) // 判断这个状态是否为-1,代表你当前并不不能去尝试获取锁,要被阻塞
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) { // 如果这个状态大于0,说明当前线程被中断
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do { // do while删除队列中状态大于0的线程结点
node.prev = pred = pred.prev; // 将node的前一个结点指针重新设置,并且让pred重新赋值为一个状态不是大于0的结点
} while (pred.waitStatus > 0);
pred.next = node; // 将pred结点的下一个指针指向当前线程结点
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // 否则将前一个线程结点的状态设置为-1
}
return false; // return false
}

假如返回false的话就再次执行循环,尝试获取锁,这就是第二次自旋操作!!!

如果第二次自旋还是获取锁失败的话,又将进入这个方法,此时当前线程的前一个结点的waitStatus为-1,直接返回true,导致parkAndCheckInterrupt()这个方法执行!!!

1
2
3
4
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 阻塞当前线程
return Thread.interrupted();
}

到这里线程的加锁才真正的结束!!!到现在还有一个方法还未说明,不知道心里想到了吗!!hasQueuedPredecessors()就是这个方法,到现在才说因为要把前面这些弄懂这个方法才比较好明白!!

hasQueuedPredecessors()方法

这个方法是tryAcquire中的判断自己是否需要排队

1
2
3
4
5
6
7
8
9
10
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t && // 队列不为空,但是当前线程是队列中第一个排队的那个线程。
((s = h.next) == null || s.thread != Thread.currentThread());
}

到此非公平锁加锁完毕,非公平锁大致流程也一样,就是除了我上述说的那些不一样之外都相同。

总结

AQS就是用一个volatile修饰的state标志来记录锁相关的状态,加上一个阻塞队列。如果这个标志位是被占用状态然后此线程就阻塞入队,空闲状态就可以获取锁,这里获取锁成功的含义就是你那个线程执行的lock()这个方法能正常返回,然后它自然就可以向下执行了,加锁不就是阻塞住,不让lock()方法正常返回吗。不过在阻塞这个过程中存在很多细节,并不是直接将线程阻塞住(这样的话不就和1.6之前的synchronized差不多啦嘛,当然是在存在线程竞争情况下,非竞争情况下还是lock是Java API级别较快),而是进行自旋操作再次尝试获取锁(不知道你感觉这个1.6版本的synchronized锁升级差不多吗)。加锁就是利用的Unsafe类的park()方法。

然后这里简单总结一下。

  1. 阻塞队列的第一个结点的线程总是为空。
  2. 当前持有锁的那个线程总是不在队列当中。

那个第一个结点应该模拟的是当前持有锁的那个线程,就像食堂吃饭排队一样,你能说在窗口打饭的第一个人在排队吗???

只有第二个人才是第一个在排队等待的人,他才有资格询问一下该不该轮到我了。