就是返回了底层的state变量,也就是AQS的state
[](()hasQueuedPredecessors方法
这个方法是用来判断线程是否需要排队的(返回false不需要排队),因为是公平锁,所以这个判断是必须的,否则就不公平了,因为没有判断排队操作,那么只要一个线程释放了锁,那么后来的线程也是可以抢到的(透露一下:非公平锁的实现就是仅仅少了这个判断而已)
可以看到,这个方法是AQS里面的,tail和head属性前面已经提到过,是底层队列的头尾结点
关键在于return语句
他主要有两个判断
-
头结点是否等于尾结点
-
头结点是否尾空,或者头结点的线程是否为当前线程
关于这个弹出,必须要先清楚,底层队列里面的结点是怎么添加的
下面就是AQS的插入方法addWaiter
可以看到,如果尾结点不为空,也就代表不是第一次插入,那么直接就使用尾插法,如果是第一次插入,只是调用enq方法,所以enq方法是针对第一次插入使用的,这两个方法都是AQS的
可与看到里面是一个死循环,如果是第一次插入,让创建一个新结点为头结点(相当于一个哨兵),然后让新尾结点一样,这一点可以看成是初始化头尾结点,接下来下一轮循环,此时尾结点已经不为null,接下来就让新结点指向尾结点,让新结点又称为了尾结点,再让旧的尾结点指向新的尾结点
但这里有一个疑问,如果并不是第一次入队,为什么最后还要执行多一次enq方法呢?
我们可以看到,如果并不是第一次入队,因为前面的addWaiter,使用了一次CAS,那如果CAS失败了怎么办?即中途有线程把尾结点改了怎么办?
此时就需要重复的操作,不断的执行CAS直到成功为止,所以就有了无论哪次插入都要执行enq方法,因为enq方法里面就是一个死循环的CAS执行,直到CAS执行成功,才会结束
总结一下添加结点
-
所以头结点只是一个哨兵,是不存储线程的,而插入的方法则是尾插法
-
其实头结点也不完全是哨兵,下面我们看线程自旋时,会发现其实头结点是当前轮到执行的线程,这里先埋下个伏笔
知道了插入方法后,我们回过头来看hasQueuedPredecessors的返回值,也就是如何判断是否需要排队
步骤如下
-
判断头结点是否等于尾结点,只有在初始化的时候,头结点才会等于尾结点,或者队列为空,头尾结点都为null,所以,如果头结点等于尾结点(直接返回false),就代表了队列为空,所以就不需要进行排队
-
通过第一个判断就已经知道头尾结点之间是有其他结点存在的,但由于前面的enq方法是先处理头结点的,然后除了两个CAS操作之外,其他都不是原子性的,也就是基本上整个enq方法都不是原子性的,所以当正在插入第一个线程的时候,会出现一个h.next == null的问题(这时已经代表有一个线程正在插入了,但还没完成插入,所以当前线程需要排队),所以当h.next == null时就代表要排队
-
最后一个判断就是判断头结点的下一个线程是不是当前线程(也就是下一个是不是轮到你,不需要排队,因为插入方法是尾插法,所以弹出方法要从头弹出),经过前面两次判断,可以得知队列中有人在排队,并且此时并不存在队列第一次插入结点的Null问题,那么最后就需要判断下一个是否轮到当前线程
[](()setExclusiveOwnerThread方法
当前面判断无人排队,或者下一个就是轮到当前线程,那么首先要做的是调用setExclusiveOwnerThread方法
这个方法唯一执行的是:将exclusiveOwnerThread改成为自己,表明这个锁被这个线程获取了
[](()总结tryAcquire方法
-
首先获取当线程
-
获取锁的当前状态,0代表无人占用
-
如果无人占用,判断当前线程是否需要进行排队
-
如果队列为空,不需要排队
-
如果队列不为空,但下一个是轮到自己,也不需要为空
-
这里要注意,第一次插入的null问题
-
如果当前线程不需要进行排队,下一步就是执行CAS来获得锁(此时锁state的状态要由0变为了1),CAS保证了同时争夺锁的并发安全性,如果此时CAS失败,就要重头来(本质上是CAS修改state从0变为1)
-
如果抢到了锁,那就将锁的拥有者改成自己
-
返回true给上一层
-
如果锁已经被占用了,也就是state不为0,分为两种情况
-
其他线程占有这把锁,所以后续操作直接返回false
-
当前线程占有这把锁,又再次进行加锁,相当于加了多层锁,又要去修改锁的状态state,一般来说会加1(源码上是加上acquire,而acquire传进来是为1),代表这个锁进入了需要被释放多次,才可以被其他线程获取,然后返回True给上一层
-
如果是第二种情况,是不需要使用CAS来保证线程安全性的,因为上一层已经有一层锁在保护这个线程的安全性
整个判断是否排队的tryAcquire方法已经结束了
接下来就是回到上一层的acquire方法
[](()返回AbstractQueuedSynchronized的acquire方法
可以看到,如果需要排队,就会执行acquireQueued方法,接下来在执行addWaiter
addWaiter方法已经看过了,其实就是一个结点进入底层队列而已,返回的是插入的结点,相当于是尾结点,也就是入队操作,不过这里离要注意的是他的参数是一个Node.EXCLUSIVE,我们接下来来看一下他究竟是什么
其实本质上这只是一个状态表示,表示入队这个结点是进入等待状态
[](()acquireQueued方法
源码如下
接下来我们看看这个方法干了什么
前面已经判断了是否需要等待,如果需要等待,就会先执行插入队列的方法,然后执行acquireQueued,既然进入了队列了,那么此时就应该是park此时的线程,也就是停止此时的线程,但其实停止并不是休眠或者阻塞,在ReentrantLock中,是使用自旋的方式来进行的
其实,这个方式其实就是ReentranLock底层的自旋操作
接下来分析整一段源码
final boolean acquireQueued(final Node node, int arg) {
//定义一个failed变量
//该变量用来判断当前线程拿到锁是否失败
boolean failed = true;
try {
//定义一个interrupted变量
//这个变量用来判断当前线程是否还在排队(还在自旋)
boolean interrupted = false;
//死循环
//这个死循环就是底层的CAS自旋
for (;😉 {
//获取当前线程结点的前一个结点
final Node p = node.predecessor();
//如果前一个线程是头结点,然后当前线程就会去看是否需要排队
//前面已经提到过,头结点的线程要么是一个哨兵,要么就是当前拥有锁的线程
//如果前一个线程是头结点,那就代表当前线程就要时刻注意前一个线程是否结束
//所以要死循环不断地去看需不需要排队
//如果前一个结点并不是头结点,因为是公平锁,那就不需要在意,继续等待
if (p == head && tryAcquire(arg)) {
//进入到这里,就代表前一个线程已经结束了
//并且当前线程抢到锁,正在执行
//先将自己改成头结点,代表自己正在操作,让下一个线程注意
//并且这个setHead不是普通让自己成为头结点
//他会将自己里面的线程设为
//因为这是要满足一个原则:线程队列里面不允许存在拥有锁的线程
setHead(node);
//断开原来头结点与自己的连接
p.next = null; // help GC
//failed变量设为false
//代表线程拿到锁
failed = false;
//返回interrupted
//该线程结束自旋,开始执行
return interrupted;
}
//当争夺锁失败时
//调用shouldParkAfterFailedAcquire方法
//这个方法是检查和更新未能获取锁的线程状态,判断是否需要进行阻塞
//如果需要阻塞就会调用parkAndCheckInterrupt
//也就是让这个线程进行阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//获取不到锁,failed依然为true
//获取不到锁,还需要继续自旋,执行下一轮循环
interrupted = true;
}
} finally {
//当轮到线程执行时,需要结束自旋,开始执行动作
//即failed会变为false
//但首先要做的是,
if (failed)
cancelAcquire(node);
}
}
前面的自旋循环很简单,但是一直自旋下去并不是好事,因为自旋也是会消耗CPU的,假如有一个线程迟迟不释放锁,就白白消耗了很多的CPU,所以,自旋是要限制次数的,而限制次数就存在于当获得锁失败时可能会调用的两个方法
[](()setHead
可以看到,setHead方法将头结点里面的线程设为了空,因为要满足获取锁的线程不存在于队列中,代表获取锁的线程已经不需要再进行排队了,而且不需要被叫醒
[](()shouldParkAfterFailedAcquire方法
前面已经提到过这个线程是用来判断获取锁失败的线程是否需要park,即是否需要阻塞(因为CAS太多次了)
我们先来看看源码
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前一个线程的waitStatus状态
int ws = pred.waitStatus;
//如果前一个线程状态为SIGNAL
if (ws == Node.SIGNAL)
/*
-
This node has already set status asking a release
-
to signal it, so it can safely park.
*/
//返回true,即当前线程需要阻塞、休眠
return true;
//如果前一个线程的状态大于0
if (ws > 0) {
/*
-
Predecessor was cancelled. Skip over predecessors and
-
indicate retry.
*/
//代表前一个线程取消执行了,就要从队列中删除前一个线程
//当然这是一个循环,可能前面有其他取消的线程,那么也是要删除的
do {
//让当前线程的前一个线程为前前一个线程
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} 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.
*/
//如果是0或者其他状态
//那么前一个线程还是可以继续自旋的,不过将状态改成了SIGNAL
//当前线程下一轮抢锁如果还是失败,那他的上一个线程就会因为变成了SIGNAL
//而被取消线程,转而变成阻塞了
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//不需要park掉线程
return false;
}
那么我们来看看这个SIGNAL状态是什么
这个值为-1,线程的waitStatus为-1就代表了这个线程不可以自旋了,要阻塞、休眠了,所以也可以waitStatus为-1也可以表示线程休眠
这里就会有一个问题,为什么要让队列中的上一个线程去决定当前线程是否需要睡眠(为什么要去修改上一个线程的waitStatus为SIGNAL),即挂起当前线程??
这是因为前面的都等的都要休眠了,后面的更加需要进行休眠(前面的都等的睡着了,后面的也可以睡着,毕竟公平锁要前面的执行完才会到后面的),但其实这个waitStatus只是一个标志性睡眠,不代表线程真的被挂起了,
那么又有一个问题,自旋几次才会进入休眠状态??
答案是自旋两次,第一次自旋,抢不到锁,将上一个线程视为睡眠,也就是将其waitStatus状态改为SIGNAL,第二次自旋,发生前面的线程已经是睡眠了,那么就挂起
[](()parkAndCheckInterupt方法
这个方法其实就是让线程挂起,只有判断了线程需要挂起才会去执行
这个方法的作用是,让线程进入等待状态,等待状态可以被两种动作唤醒
-
其他线程尝试对该线程进行interrupt
-
被其他线程进行unpark
如果是被其他线程进行interrupt唤醒的,就代表有其他线程想终止当前线程,那么Thread.interrupted就会返回true,如果不是,是第二种方式的,就会返回false
可以直到其调用了LockSupport.part方法
这个方法其实就是禁用线程的,也就是将线程挂起

[](()cancelAcquire
可以看到,从acquireQueued方法里面,进行了一系列try语句里面的流程之后,最后还有一个finally语句块,这个语句块是调用了cancelAcquire的,而且要在failed变量为true的时候才会执行
现在问题来了,failed变量在自旋死循环中,只会变成false,而不会变成true,而且线程挂起之后更加不会走到这行代码,那么什么时候会调用这个acquireQueued方法呢?
我们可以知道,这里要执行acquireQueued方法的唯一途径就是try语句块里面抛出了异常,中断了try里面的代码,而且try里面的代码并没有将failed修改为true
那么这个抛出异常的地方在那里呢?
回顾一路走过来,抛出异常处只有一个地方
也就是当调用tryAcquire去看是否需要排队时,当拥有锁的线程又再一起去申请获得锁,可能会抛出异常,不过这个异常抛出也是有前提的,也就是锁的状态要小于0?
我也不太清楚什么情况会让锁的状态小于0
接下来看看这个cancelAcquire方法做了什么
这个方法实际上就是让这个线程取消获取锁
源码如下
private void cancelAcquire(Node node) {
// Ignore if node doesn’t exist
if (node == null)
return;
node.thread = null;