一、用法
首先看ReetrantLock的用法。
private ReentrantLock lock = new ReentrantLock();
lock.lock()
try{
操作
}catch(){}
finally{
lock.unlock();
}
我们在需要加锁的操作前面使用lock()方法,然后进行操作,在fianlly里面释放锁。那么lock,unlock里面是怎么做的呢。这里就需要讲到AQS(AbstarctQueuedSynchronizer)。ReetrantLock底层是依赖AQS实现的。
二、AQS基本属性
首先。我们知道锁就是对线程进行操作,AQS对线程进行了包装,封装成Node类,里面包含Thread。我们先看下Node类的结构。
这里是Node类定义的属性。
1.SHARED,EXCLUSIVE是Node类型,他们分别表示共享锁和排他锁,这里针对不同的类型,会有不同的逻辑,比如ReetrantLock就是实现排他锁,CountDownLatch,Semaphore就是实现共享锁。
2.CANCELLED,SIGNAL,CONDITION,PROPAGATE表示的线程的状态。
(1)CANCELLED表示该线程已被取消,抢占资源时不用考虑它。
(2)SINGAL,当线程的状态是这个时,它表示这个这个线程Node后面链接的node里面的线程需要被唤醒。
(3)CONDITION,当线程用到lock里面的Conditon类时,才会设置这个值,表示在等待Condition唤醒。
(4)PROPAGATE是共享锁的时候才会用到,表示这个线程获取到共享锁。这里就不赘述了。
3.waitStatus,表示线程的状态,上面的CANCELLED,SIGNAL,CONDITION,PROPAGATE就是表示当waitStatus分别为1,-1,-2,-3的意义。
4.prev,next表示当前节点的前节点和后继节点。所以 AQS维护的等待队列是一个双休队列
5.thread 是这个节点存放的线程。
6.nextWaiter 两个作用。
(1)用来区别当前队列是独占锁队列还是共享锁队列,如果nextWaiter=SHARED,那就是共享锁,当nextWaiter=EXCLUSIVE=NULL时,说明是排他锁。
(2)当其用到condition方法时,会在nextWaiter后面追加节点,形成单向的条件队列(后一章讲到)
Node 节点提供的方法如下
1.isShard()是判断该节点的线程竞争的是共享锁还是独占锁
2.predecessor()返回该 节点的前一个节点
3.Node(Tread,Node)构造节点,mode表明该线程竞争的锁是共享锁还是独占锁
4.Node(Tread,int)构造节点,waitStatus表明节点的状态
讲完Node后,我们看看AQS里面提供的基本属性。
里面提供了东西有:
1.Node类型的head,tail属性,因为我们需要维护一个等待队列,所以维护了一个指向头尾的指针。
2.有个int型的state值,它就是AQS的状态,也是各个线程争抢的资源,所谓的锁锁住临界资源,就是在临界资源竞争前,使用lock方法,让各个线程取获取AQS里的临界资源,能取到,那就获取到了锁,没有,那就等待。
三、Lock操作
3.1整体流程
讲完基本属性后,我们开始通过ReentrantLock的lock方法,开始讲如何进行锁操作。
首先我们调用ReetrantLock类中的lock()方法。
会发现里面本质是调用sync的lock方法。那sync是啥了?
会发现sync是一个Sync类的对象,而Sync类是一个继承AQS类的内部类。至于为啥设置成内部类,我的理解是不用把AQS的方法都暴露出去。只暴露我设计好的几个方法。
但是我们用sync不是直接用Sync,而是在它的基础上又封装了一层,成两个类,
这里这样做的目的是因为公平锁和公平锁有不同的逻辑,所以设置两个不同的类。这里我们看下ReentrantLock的构造方法
发现它的默认构造就是非公平锁,只有你在入参里面传入true时,才会声明公平锁。为啥这么做,是因为非公平锁效率更高,所以默认使用非公平锁。为啥非公平效率更高,后面讲(挖坑1)。
下面我们先来看看公平锁是怎么实现的。
这里我们先看lock方法,tryAcquire一会会讲到它。在lock中,会发现它实际是调用AQS中的acquire()。并设置入参为1。
下面我们跳转到AQS的acquire方法看看。
这里其实就是四个方法。挨个说:
1.它首先调用tryAcquired方法,这个就是刚刚FairSync中的方法。该方法的作用是尝试获取锁,如果成功返回true。当它为true的时候,说明获取到锁,后面就不用进行了。当它为false,说明没有获取到锁,那么进行下一个方法。
2.addWaiter方法作为acquireQueued的入参,是先于acquireQueued方法执行的。addWaiter方法的目的是把该节点加到等待队列的队尾。
3.acquireQueued方法的目的是线程在等待队列中排队,等待获取了锁,就跳出这个队列了。
4.selfInterrupt方法是对线程的中断标志位的一个设置,因为acquire不支持异常中断,如果中途因为中断导致线程被唤醒,那么我们在最后会告诉下这个线程被中断过。
整体流程就是当前如果获取不到锁,就会加到等待队列中,进行休眠等待,会通过acquireQueued方法尝试获取锁。如果中途被中断,就会返回true,然后执行selfInterupt(),进行自身中断,设置中断状态。
3.2 tryAcquire方法
下面我们具体看着三个方法,其中tryAcquired方法,是继承AQS类自己实现的方法,比如Reentrantlock就是提供了该方法的具体实现。为啥这样呢?是因为该方法的目的线程初次获取锁,不同实现AQS类的具体类都会有自己获取锁的方式,所以这是有具体实现类自己实现。
我们来看Reentrantlock的tryAcquired的具体实现。(这里把过程解释放在注释里了,这里就不赘述了)
这里用到了hasQueuedPredecessors方法判断等待队列是不是空或者这个节点是等待队列中的头节点。(这里的头节点指的是head节点的next节点,后面同样这样称呼,和head节点做区分)
hasQueruePredecessors()方法,我们期待的是返回false,如果是false,那就说明可以获取锁。那我们看啥时候会返回false。
1.首先,h!=t,如果h==t,那就说明等待队列为空或者只有一个节点,同时是head和tail。这时候该方法返回值为false,满足要求。两者都为空,很好理解,初始化AQS的时候head和tail就是null。至于啥时候只有一个节点,后面讲(坑2)。
2.如果h!=t,说明等待队列里面有值,那么我们需要看当前节点是不是等待队列里的头节点(head节点的下一个节点),如果是,也就是说当前线程是那个节点的线程,那么没问题,返回fasle,如果不是,那么返回true,不能获取锁。这里没问题。但是这里有个问题就是s==null这句,为啥当这个成立的时候就返回true,说明自己不是等待队列里面的头节点呢?这个后面解释(坑3)
当hasQueruePredecessors方法返回false后,可以往下执行,执行compareAndSetState方法。
compareAndSwapInt是sun.misc.Unsafe类中的一个本地方法,表明以CAS方式更改数据,保证其原子性。这个底层是cpu实现的,这里就不详细讲了。入参expect表示原来的值,update是更改后的值,表示如果当前线程的状态是expect,那么就把它更改为update。
更改状态成功,说明获取到锁,然后设置持有这个锁的线程是当前线程。
该类表示获取AQS中排它锁的持有者的线程是谁。
以上的tryAcquire()方法总结起来就是,AQS状态如果是0,表示没有线程获取到锁,那么当前线程尝试获取锁,如果没有等待队列或者它是等待队列队首,通过CAS操作更改状态,表明获取到锁。如果获取到锁的线程是当前线程,那么就可以继续获取到锁,并将state数量加一。
3.3 addWaiter方法
看完tryAcquire(),当它返回true的时候,!tryAcquire为false,直接退出了,说明获取到了锁。当返回fasle的时候,下面执行addWaiter()方法。我们来看这个方法。
首先创建一个node对象,种类根据入参mode是排他锁,所以用当前线程创建一个排他锁类型的对象。
让pred等于尾节点。
(1)如果pred!=null,说明等待队列有节点。那么我们就需要把当前节点添加到等待队列最后一个节点之后。首先让新生成的node节点的前一个节点为队列最后一个节点。但是如果想把这个节点链接到队列中,还需要让队列的最后一个节点的next节点为这个新节点。这里调用compareAndSetTail,让tail节点等于新生成的node节点,这里为啥用CAS呢?因为这是多线程环境,可能这个时候有个线程也执行了node.prev=pred,对它而言认为的tail节点就是pred指的节点。可是这个时候你执行了compareAndSetTail,然后tail就变成了新node 节点的位置了。如果这个时候不用CAS操作,它会直接执行pred.next=node,而对它而言的pred是你这个线程的节点的前一个节点,这样一next,就把你跳过了。所以加个CAS,会判断tail之前是不是pred,如果是,才赋值成node。
(2)如果pred是null,说明队列为空,这个时候我们需要新建一个队列。并将该节点放到队列中。下面我们来看enq方法。
其中,当t==null,也就是等待队列为空时,通过compareAndSetHead()方法创建一个头节点,
这里为啥使用CAS操作,同理也是因为这是多线程环境,可能同时有两个线程在创建头节点,而AQS中只要维护一个头节点。
创建一个头结点后,让tail等于head节点。tail由之前的null,变成现在和head一样,里面存放一个初始化的node节点。所以你会发现head节点,里面没有存放啥有用的信息,所以它是一个傀儡节点,它所指向的节点才是队列中真正有意义的头节点。到这里,这个等待队列里面有一个head节点,但这个队列还是为空的,里面没有存放任何有线程的node节点。
下面我我们来回顾下上面的hasQueruePredecessors()方法,来解决挖的坑2,3.
前面介绍这个方法的目的是判断这个线程所在节点是不是等待队列的头结点。返回false表示有资格获取节点,那么什么情况下会返回false,首先就是h==t,那啥时候会相等了,就是等待队列为空的时候,但这个为空,其实是两种情况,一种就是tail==null,head==null.这个就是AQS刚被创建出来的时候,这两个也是相等的。第二种情况就是上面说的情况,我刚创建了head节点,让tail等于head,这个时候他两也是相等的,但是这个时候等待队列里面没有有线程的节点,所以也是空的。(已填坑2)至于s.thread != Thread.currentThread()很好理解,s是head节点的下一个节点,也是真正有意义的头结点,所以会判断这个节点是不是当前线程的节点就可以知道当前线程节点是不是等待队列的头节点,至于s==null这个判断,同样可以分析,首先明确在进行addWaiter方法入队列操作的时侯因为没有获取锁,所以这块是可能多线程并发进行的。那么我们看
这步,当我入队的节点就是head后面的第一个节点,它tail改变指向到他自己,接下来应该就需要把head的next指向自己了,但是这个时候有个其他线程也进行tryAcquire操作,然后进入到了hasQueruePredecessors方法里,这个时候虽然head的next是null,但是对于这个线程而言它不是等待队列中的第一个节点,因为已经有节点加进来了,只是还没修改head的next指向,所以这块加入了一个(s=h.next)==null判断,如果成立,说明不是等待队列头结点,返会true.(已填坑3)
回归到enq方法,创建head节点后,会结束这次循环,进行下循环,然后这个时候tail就不为空了,然后就可以把当前线程的节点入队了。这里和addWaiter()方法的前半部一样,就不分析了。当失败的话,通过for(;;)方法不断循环,直到入队成功。这也就是加for(;;)的目的。
看到这里,会想啥时候进行调用enq()方法,addWaiter()在两种情况下会调用enq,一种就是上面说的tail为null,还一种就是在尝试把节点入队时用的是compareAndSetTail(pred, node)方法进行CAS操作更改节点指向,如果这个时候有其他线程也在操作tail,那么这个方法会返回false,说明更改tail指向失败,也就是入队失败,那么这个时候同样会执行enq。
看到这里,又有个问题,你会发现enq其实本质上就是节点入队的一个方法,它把addWaiter方法的前部分功能也做了,那么这里为啥要写addWaiter的前半部分,这里我个人的理解是因为在大部分情况下,等待队列是有节点的,而且不会那么多的多线程竞争的情况下发生,所以会尝试先进行一次入队,如果成功那就ok了,如果不成功,那么我在调用enq方法,通过不断循环的方式,再入队。这样效率会高些。
3.4 acquireQueued方法
到这里我们就讲完了addWaiter()方法了。完成了节点入队,下面进行acquIreQueued方法的分析,该方法的目的就是在等待队列中进行等待,有机会就去抢占锁。
先明确入参,这里的node是刚刚刚入队的node节点,arg对于ReentrantLock而言,是1。下面一点点看代码。
先不看failed,interrupted变量。先看执行逻辑。首先用p指向当前入队节点的前一个节点,首先判断p是不是head节点,如果是,那么根据我们知道的head节点是傀儡节点,它的下一个节点才是等待队列的头结点,那么这个node节点就是在等待队列的首部,那么它就有资格去获取锁,所以调用tryAcquire方法,如果成功了,进行下面的逻辑,首先调用setHead()方法,
setHead方法很简单,就是让head往下移一位,让当前线程的节点成为head,并清空线程,以及前指针,使其成为傀儡节点。这个就是相当于头结点出队了,因为它获取到了锁,不用在队列里待着了。这里又有个问题,大家都知道,出队操作,我们一般是让它的前一个节点指向后一个节点,这样这个节点就被删了。但是这里为啥是让head结点后移,并改节点内容,使其成为head节点。我的想法是觉得如果是使用前一节点指向后一节点的方式,那么当这个头node节点就是队列中的尾,我们还需要额外的操作进行判断,否则tail就丢失了,使用它提供的方法,更具有通用性。这里不用CAS操作,因为调用到这里的线程已经获取到锁了,所以不用CAS.
setHead(),之后,删除p的next指向,也就是原来head节点的指向,使原来的head节点成为一个孤立的节点,这样GC就会去清理掉它。然后往后看,这里用到了一个failed变量,并置为false,这是为啥了(坑4),以及为啥返回interrupted值(坑5),这个我们一会讲。
往下进行。当你不是head指向的第一个节点或者tryAcquire失败,那么你就需要在队列中进行等待。这里就到了shouldParkAfterFailedAcquire(p, node)方法,下面看看这个方法,
看方法名称,叫是否在失败获取锁后挂起线程,这也是方法的目的。
首先明确入参pred是当前线程节点的前一个节点,node是当前线程节点。
ws取pred状态。然后用到了SIGNAL,它的作用是啥,就是你这个线程可能是被挂起了,可能是你等着等着不想等了,想取消在等待队列里等待了,(目前就考虑这两个状态)那么你就需要一个状态值来表示你这个线程的状态。SIGNAL的作用是如果你这个线程被挂起了,你要告诉你的前一个线程节点,你被挂起了,等到它出队的时候,要把你唤醒。所以这个值是放在你的前一个节点上的,这样前一个节点出队的时候,看自己的waitStatus是SIGNAL,就知道自己下一个节点需要被唤醒。所以这里的shouldParkAfterFailedAcquire方法的目的就是对前一个节点的waitStatus进行更改的,改成SIGNAL。
1.首先如果前一个节点是SIGNAL,那么OK,不用改了,直接返回true,
2.如果不是,我们需要改一下,但是你前面的线程可能是被取消的线程,它的状态被设置成CANCELLED, 那么我需要找到离我最近的不是取消状态的节点,然后设置它的waitStatues为SIGNAL。ws如果大于0,这里记住节点的几个状态值,初始化的时候,默认就是0,大于0的只有CANCELLED状态,SIGNAL为-1,小于0.所以ws大于0,就是前一个节点为取消状态,通过node.prev = pred = pred.prev;方法,让你的前指针指向前前节点,直至找到不不是取消状态的节点,然后让这个节点的next指向你,这样就把中间所有的状态为取消的节点删除了。然后到这里它就退出这个方法了。然后回到acquireQueued方法,因为shouldParkAfterFailedAcquire返回false,所以会结束这次循环,接着进行下一次循环,这里也是用for(;;)的目的,让你这个节点在队列中不断循环,知道获取到锁并出队。
3.回到shouldParkAfterFailedAcquire方法,你再次进行for循环的时候,会再次进入这个方法,因为你刚刚只是把你前面的状态为取消的节点删除了,但是没有设置前一个节点状态为SIGNAL,所以这里会执行compareAndSetWaitStatus方法,通过CAS操作,把前一个节点的状态从ws变为SIGNAL,在ReentrantLock中,其实就是把值从0变为-1.到这里,它的前一节点就是SIGNAL,然后下一次循环的时候,通过ws==Node,SIGNAL就会成功,返回true。说明可以进行挂起当前线程了。
这里其实有个疑问,我找到第一个离我最近的不是取消状态的节点,直接设置它的状态为SIGNAL不就好了,为啥还有退出,进行for(;;)循环,然后再次进来进行修改。我的看法是,它把这几个步骤分开,就是怕在你操作的时候,你可以获取到锁了,但你不知道,你还老老实实往下走,通过parkAndCheckInterrupt挂起自己,这样浪费调度资源。所以这里就让你自旋的尝试几次自己是不是头结点,并获取锁,如果还不行,再挂起它。
然后进行parkAndCheckInterrupt方法,进行线程挂起。
这里通过park方法挂起线程,直到这个线程被唤醒了,才继续往下进行,进入acquireQueued方法,再判断是不是头。但是有个问题,这里为啥要返回Thread.interrupted()。因为这个线程除了被唤醒以外,它可能是被中断了,这样也会往下执行,所以这里返回是否被中断这个判断。当它被中断,我们会返回true,acquireQueued方法中,取到true,就会设置interrupted变量为true,表明这个线程是被中断唤醒的,然后这个线程再去获取锁,成功后,我们会return interrupted的值为true,说明这个线程被中断了(已填坑5)。这里一会可以对比doAcquireInterruptibly方法,它是当我们调用ReentrantLock的lockInterruptibly方法是调用这个。
你会发现和acquireQueued的唯一区别在于它抛出了异常,这样它就到了finally的方法体了,进行cancelAcquire方法了。而不会再次获取锁。
看到这里,我们再来讨论下failed这个变量的意义(填坑4),它就是用来判断是不是获取锁成功,因为只有在获取锁成功后,才会改为false,其他时候为true,然后结合finally看,啥时候会执行到finally中的cancelAcquire方法,因为正常return的时候,failed被设置为false,不会执行cancelAcquire,发现只有在try中发生异常的时候才会进行finally操作,对于doAcquireInterruptibly方法,它是会抛异常的,所以有可能执行cancelAcquire,但是在acquireQueued方法中,它没有抛异常,为啥还这样写呢?我的理解是这里留了一个口,因为tryAcquire这个方法是由具体实现类自己实现的,如果我们自己想写个排他锁,重写这个方法,那么它是有抛异常的可能的,那么这个时候就会调用cancelAcquire方法了。对于ReentrantLock这个类而言,其实lock方法是调不到这个方法的,因为它的tryAcquire不存在抛异常。
下面我们看看cancelAcquire方法做了啥。
这个方法挺长的。我们慢慢看。看名称,cancelAcquire意思就是把这个节点在队列中取消。
1.首先如果为null,那不用看了,直接返回了
2.
通过该过程,找到离你最近的不是取消状态的节点,并让你的pred指向它 ,然后声明一个predNext对象,让他成为离你最近的不是取消状态节点的nxet节点,然后设置当前线程节点的状态为取消。
会发现后面还有很长的一部分,它们是干啥的呢?其实进行初步尝试把你这个被取消的节点从等待队列中删除。这里有个问题,这里为啥要尝试进行删除,如果不删的话,因为已经把该节点的状态设置为取消状态了,在shouldParkAfterFailedAcquire方法中也会进行删除。(忘记的可以往上翻,看这个方法如何删除的)。这里我个人的理解是也是提高效率,被设置取消状态后,就进行第一次尝试删除,如果成功了,就不用等到下一个节点入队时删除。下面我具体看看这块是如何删除的。
1.首先看你这个被删除的节点是不是尾节点。如果是,把tail往前移到pred位置,这里pred是离你最近的不是取消状态的节点,因为我们不能丢了tail。如果成功了,这个时候pred点就是队列中的尾节点,我们需要把它的next节点设置为null。
2。然后看这个节点是不是中间节点,就是它既不是head节点指向的第一个节点,也不是尾节点。如果是,也就是满足pred!=head,接着往下看,看满不满足((ws = pred.waitStatus) == Node.SIGNAL ||(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)))这个式子,这个式子的意思是离我最近的不是取消状态的节点它的状态是不是SIGNAL,如果是,那么OK,如果不是,看是不是小于0,这里对于ReentrantLock没有意义,因为它的状态只有代表1的CANCELLED,0的初始状态,-1的SIGNAL状态,只有用到剩余两个状态是才会执行到这里,这里就先不说了。所以如果离我们当前节点最近的不是取消节点的状态是SIGNAL的话,那么状态成立,我们接着往下,看pred.thread != null是否成立,保证这个线程是有效的。然后,我们取当前节点的next节点,判断是不是不是取消状态,如果是的话,让pred节点next指向当前节点的next,这样当前节点就被删除了。看到这里可以思考下为啥声明个对象predNext,让他指向pred的next节点,目的是用CAS操作进行指针下移。
3.如果这个节点是head的下一个节点,那么进行unparkSuccessor方法。
看下这个方法,首先明白这个入参是head下的第一个节点。这里看起来很复杂,为啥,因为我不仅要删掉这个位于head下的第一个节点,我还需要唤醒这个节点后面第一个不是取消状态的节点。下面具体分析下代码。
1.首先判断node节点的状态是不是SIGNAL,其实在刚刚调用方法的入口而言,不会调用到这块,因为调用这个方法前node的状态已经被设置为取消状态(1),这里写这个的目的不是给cancelAcquire调用的,是其他方法调用时会有可能调用到这块。(这个以后说)。
2.我们往下看,取s=node.next。如果s==null,那就直接结束了。当它的状态是大于0,也就是取消状态,我们通过for循环,从后往前找。找到离node最近的一个不是取消状态的节点。然后如果不为空,那么唤醒这个被找到的节点。这里又会发现一个问题,为啥要从后往前找,不是从前往后找,很明显从后往前找效率低。这里因为从前往后搜索有安全问题,同样是因为addWaiter方法,它进行节点入队时,是先设置这个节点的pred,然后CAS移动tail,最后设置原tail的next为入队节点。所以这块寻找next由于并发的原因,不一定会一定有,但是它的pred一定有,因为它是先操作的,所以这块用从后往前找的方法。
3.最后唤醒它。
到这里ReentrantLock的公平锁加锁就讲完了。上面是ReentrantLock和AQS的交互,发现ReentrantLock只要实现tryAcquire方法,定义自己获取锁的方式,其他都有AQS做了。我们以后也同样可以自己重写tryAcquire,写个自己的锁。
3.4 非公平的lock方法
这里我们看看非公平锁的加锁。
对比公平锁的加锁,直接调用acquire方法,这里多了个if调用,就是先尝试更改下AQS的state值,(更改AQS的过程就是获取锁的过程,更改成功就说明获取到了锁),更改成功,说明获取到锁,那么设置持有锁的线程是当前线程。这里也就是非公平的本质。公平就是每个想获取锁的线程,都得入队排队,而非公平就是你过来先获取下锁,如果不成功再入队。如果队列中有个线程释放了锁,这是有个线程过来想获取锁,可能就直接获取到锁了,这样就避免调度等待队列中线程的调度,这也就是快的原因。
它们在tryAcquire上还有些区别,非公平锁使用的是定义在Sync当中的nonfairTryAcquire方法,对比与公平锁tryAcquire方法,非公平锁会少个hasQueuedPredecessors方法的调用,这是为啥?首先知道hasQueuedPredecessors方法的目的是判断你的这个线程是不是等待队列中头结点,因为公平锁只有头结点才有资格获取锁,但是非公平不用这样,它会先获取下锁,不成功在入队,所以不用判断它是不是等待队列的头结点。
四、unlock方法
4.1 整体流程
巴拉巴拉这么长,终于说完了加锁,下面说下释放锁。
同理,先从公平锁的unlock引入。
发现它是调用sync的release方法。这里的release方法是由AQS提供的。我们跳的AQS中看看,
它的思路是先尝试释放一下,如果成功了,说明线程已经释放锁了。那么我就需要把等待队列中head指向的第一个不是取消状态的节点唤醒,并进行尝试获取锁,后面的过程请参考上面获取锁中的acquireQueue方法。
看下尝试释放的方法tryRelease()
这个是有ReentantLock重写的方法,因为state的值就是这个锁持有的次数,所以释放就是减state值,对于ReentrantLock,每次减1.当它减完为0的时候,就说明没有线程持有这个锁了。
回到这个方法,我们通过if判断等待队列是不是有节点,根据前面讲的,两种情况下算是等待队列为空,一种就是没有head和tail,一种是只有一个head节点,tail等于head,这个时候是刚创建的head,它的waitStatus就是0。这样就理解了上面的if (h != null && h.waitStatus != 0)的这句代码。然后我们进行唤醒后面的节点。
进入unparkSuccessor方法。
这个方法就是唤醒离head节点最近的一个不是取消状态的节点,前面已经讲过这个方法了。这里就if(ws<0)这块说明下,因为在上一个调用环境中,因为是取消节点,所以不会通过这个if,但是当用unlock方法时进行调用这个方法,会通过这个if,进行compareAndWaitStatus方法,进行head节点的状态清除,把它的状态变成0.因为我们要唤醒下一个节点,所以把状态清除。