从ReentrantLock讲AQS的Condition

一、用法

下面我们从ReentrantLock切入,讲AQS提供的Conditon一系列方法。

先看用法。

private ReentrantLock lock = new ReentrantLock();

private Condition condition=lock.newConditon();

lock.lock()

try{

操作

condition.await();//这里调用await()让线程等待,释放锁,同理一样用signal()方法唤醒线程

}catch(){}

finally{

lock.unlock();

}

首先我们知道lock()方法是获取锁,unlock()方法是释放锁,可是这中途需要进行线程调度,放弃锁咋办,所以这里引入了Condition的方法,进行线程释放锁。

二、Conditon基本属性

下面我们看具体方法,首先,我们得先声明一个Condition对象,这个对象不能直接new Condition(),的调用lock的newCondition方法,看这个方法底层,

实际就是new ConditionObject()对象,因为该类实现了Condtiion接口,实现了await(),signal()等方法。

我们看看这个ConditionObject类里面提供的属性。

1.这里的firstWaiter,lastWaiter是两个node对象,因为我们要维护一个条件对列,它是一个单向队列,所以这里用这两个对象维护这个队列。

2.这里的REINTERRUPT,THROW_IE是区分线程被中断的情况,这里照样挖个坑,之后解释(坑1).

三、await方法初次分析

3.1 整体流程

下面我们看看condition.await()方法,从上面已知,这里实际是调用AQS中的一个内部类ConditionObject的await()方法,下面我们去看看这个方法。

我们先梳理下这个方法实现功能的思路,然后再深入每个具体的方法里面。

1.首先,如果调用该方法的线程被中断了,那么不用说,不用往下执行了,直接抛异常就好了。

2.下面进行正式功能,首先通过addConditionWaiter()方法,把调用该方法的当前线程封装成Node节点并加入条件队列中。

3.然后我们通过fullyRelease()方法释放当前线程所获取的的锁。

4.之后通过while语句,判断这个调用await的线程所在的节点是不是在AQS的双向等待队列中。(这里注意区分下,AQS中通过Node中的pred,next维护了一个双向的等待队列,这个是lock方法是进行入队的队列。同时Node中通过nextWaiter维护了一个单向的条件队列,这个是await()方法入队的队列)。如果节点在等待队列中,说明已经用signal或者signalAll方法了,所以进入等待队列中,如果不在等待队列中,说明还在条件队列中,那么调用park方法让这个线程挂起。

5.如果这个线程被移到了等待队列中,那么尝试通过acquireQueued方法进行进行获取锁。

这里只是讲了大致流程。具体细节都没有分析。下面我们一点点看细节。

3.2 addConditionWaiter方法

首先我们会通过addConditionWaiter()方法,把调用该方法的当前线程封装成Node节点并加入条件队列中。我们来看看这个方法是咋做的?

首先我们判断尾节点lastWaiter是不是null或者是被取消状态的节点,如果是的话,我们调用unlinkCancelledWaiters()方法清除条件队列中状态为取消状态的节点。然后让t等于新的lastWaiter节点。

然后我们把当前线程封装成一个node节点,类型是CONDTION,表明这个node节点是在条件队列中的。

之后我们判断t是不是为null,如果是,说明条件队列里面没有节点,那么把这个新生成的节点作为firstWaiter头结点。(这里注意区分下,firstWaiter就是真正有意义的头结点,而AQS中的head是个傀儡节点,它的下一个节点才是有意义的头结点)如果t不为null,说明条件队列不为空,那么让lastWaiter的下一个节点为node,然后让lastWaiter指向node。其中我们会发现我们没有用任何的CAS操作保证其一致性,因为调用该方法时线程还获取这锁,所以不用考虑多线程竞争的情况。

其中用到了unlinkCancelledWaiters()方法清除条件队列中状态为取消状态的节点,下面我们看看这个方法。

这里用trail表示要被清除节点的前一个节点,t表示要被清除的节点。

我们从头开始遍历这个条件队列。如果t的状态不是CONDITION,那么就t.nextWaiter=null.切断它对下一个节点的指向,然后,如果trail==null,说明这个节点是条件队列的头,那么设置firstWaiter为下一个节点。否则,让当前节点的前一个节点指向后一个节点,这样这个当前节点就被删除了。

3.3.fullyRelease方法

node加完条件队列后,我们看看如何释放锁的。

这里的node就是刚刚入条件队列的节点,也是需要释放锁的线程节点。

1.首先我们通过getState方法去当前状态值,因为只有当状态值为0的时候才表示没有线程持有这个锁,这里和unlock方法有所不同,unlock方法每次只是把state值减一,只有减到0才是完全释放锁,而这里,调用await()方法就需要完全释放锁,所以会一次性把state清除到0.

2.我们通过release()方法进行释放锁,这里也不赘述了,上一章也讲过了。如果成功,那么就设置failed为fasle,表明释放释放成功了。如果没有则抛异常,说明当前调用这个方法的线程不是持有锁的线程。

这里有个细节,对于ReentrantLock而言,个人认为不会走到这一步,因为release里面会调用tryRelease方法,这个方法是ReentrantLock重写的,那里面就判断了释放锁的线程是不是获取锁的线程,如果不是,会抛出异常。

3.4 isOnSyncQueue方法

我们来看isOnSyncQueue方法,用来判断当前节点在不在等待队列中。

1.如果当前节点的状态是CONDITION或者没有pred节点,那么肯定不在等待队列中。

2.如果当前节点的next不为null,说明肯定在等待队列中。

3.这里还有其他情况,就是它的状态已经不是CONDITION了,也通过pred链接到了等待队列中了,但是还没有通过next链接上(上一章分析过这种情况)这个时候用findNodeFromTail方法,从后遍历,能找到就返回true.

锁释放后,我们就需要进行线程状态的挂起了。通过park方法进行线程的挂起,但这里有个问题,为啥要用while循环判断(坑2)?

到这里我们就先停止分析await()方法,先进行分析signal方法。

四、signal方法

4.1 整体流程

首先通过isHeldExclusively方法判断排他模式下状态值是否被占用了,如果被占用了,那么抛出异常。这里Reentrantlock重新了这个方法,

照样也是判断调用这个方法的线程是不是获取锁的线程。不是就抛异常。

如果是,那么先去条件队列头结点,如果是null,说明条件队列中没有值,那么就不存在释放了。

如果有值,我们调用doSignal方法释放条件队列的头结点。

4.2 doSignal方法

这里使用do{}while()方式,先断开first节点与后面节点的连接,相当于把这个节点在条件队列中删除,再通过while方法中的transferForSignal方法,把节点移到等待队列中。

下面我们看下transferForSignal方法做了啥?

1.首先,我们需要把node节点从条件队列移到等待队列中,它原本的状态是CONDITION,而进入等待队列,相当于重新入队,它的初始状态是0,所以我们需要把的状态值从CONDITION改为0.如果这里更改失败了,说明node节点的状态不是CONDITION了,在条件队列中的节点,状态除了CONDITION,只剩CANCELLED,说明该节点已经被取消了,那么返回false,!transferForSignal就是true,然后就需要把下一个条件队列节点进行出队操作,所以这里加了一个判断下一个条件节点是不是为null的判断(first = firstWaiter) != null,如果不为null,再次进行循环,进行节点删除。

2.然后我们就需要把这个出了条件队列的节点进行入队到等待队列中。看到这里可以理解为啥要把节点状态设置为0了,在lock方法中,节点入等待队列时节点的状态都是0,表示默认值,所以这块就相当于一个新节点入等待队列了。之后通过enq方法把该节点进行入队。(这里有个小疑问,在lock中,入队用的是addWaiter()方法,先尝试入队一次,不成功再用enq入队,这里为啥不用addWaiter,直接用enq方法?)到这里,signal操作其实就差不多了,这个方法是不负责唤醒这个节点线程的,只有在某些特殊情况下才会唤醒。

这个时候我们回忆下调用lock方法的时候是怎么入队的,我们先通过addWaiter方法,入队列,此时入队node状态为0.然后会调用acquireQueued()方法,这里会判断这个节点的前一个节点是不是head节点,如果是并能获取到锁,那么万事ok,如果不行,这里会调用一个shouldParkAfterFailedAcquire方法来把自己前一个没有被取消的节点的状态设置为SIGNAL。假设一种场景,一个节点node1入队了等待队列,它的状态会是0,等待下一个节点node2入队列时,正常情况下,会通过这个方法把node1节点的状态从0设置为SIGNAL。设置完后就会park(挂起)node2。(这里可参考上篇文章)

同理,我们回到这个问题,我们来看这个if语句,这里入队列的node节点是被挂起的,如果它的前一个节点的状态不是取消状态,那么我直接更改它的状态成SIGNAL,就好了,说明我这个节点是需要被唤醒的,省掉了唤醒它,再调用acquireQueued方法,然后再通过shouldParkAfterFailedAcquire设置前一个节点状态为SIGNAL,然后再挂起这个过程。当然,如果你的前一个节点的状态是取消状态或者更改状态到SIGNAL失败,那么就需要唤醒重新调用acquireQueued方法,然后通过shouldParkAfterFailedAcquire方法删除前面状态为取消状态的节点。

这里有个问题,transferForSignal 方法在doSignal中调用的,而对于signal方法调用而言,在ReentrantLock中必须获得锁,才能调用这个,否则抛异常。那么既然获取到了锁,为啥这里还通过CAS操作来保持一致性。这里可以看下,

它是通过isHeldExclusively方法来判断是否抛异常,这个方法是可以重写的,对于ReentrantLock方法而言,不是获取到锁的线程调用这个方法就会抛异常,但是如果我们基于AQS封装其他的锁,这里的判断条件就不一定是一定要获取到锁才能调用这个方法了,所以存在多线程竞争的可能,所以通过CAS操作进行控制。

到这里signal方法就讲完了 ,这里顺带讲讲signalAll方法。

这里前面都是一样的,区别在doSignalAll()方法不一样。我们看看这个方法。

这个方法和doSignal对比,就两个地方不一样,一个是把lastWaiter和firstWaiter都设置为null,因为该方法是把条件队列的节点都出队,所以设置为null,二是把transferForSignal方法移到循环体内了,因为该方法就是把节点添加到等待队列中,所以需要把出队的都加进去,而在doSignal中只要加一个节点就好。

五、await方法再次分析

下面我们接着回归await方法,这个方法只是讲了主体,还有挺多有意思的没讲。

这里开始填坑2,因为对于await()方法而言,调用它后这个线程节点就应该在条件队列中了,如果在等待队列的话,就说明被signal过了,移到了等待队列了,这样就可以用等待等待队列的acquireQueued方法去对待它了,不用再因为是条件队列而去挂起它。但是这里为啥要用while语句呢?我们可以看到,当它被signal方法调用后唤醒,就会被移动等待队列中,那么直接不满足这个判断条件,就可以退出了,如果是被中断唤醒的,那么通过break,也可以退出while。个人的看法可能是节点在等待队列中时,其他线程不是通过signal或signalAll方法移动这个节点,而是直接unpark这个节点,那么这里的while就有意义了。(欢迎其他不同想法)

之前讲到线程被加到条件队列中,然后被park挂起了。正常情况下有两种情况被唤醒,一个就是signal方法(上面说的),另一个就是在等待队列中被unlock方法唤醒。

除了这两种,它同时也会被中断唤醒,这里我们讨论的就是这种异常情况。

首先我们通过checkInterruptWhileWaiting方法判断这个线程是被中断唤醒的,还是正常唤醒的,如果是正常的,那么返回值为0。如果是中断唤醒的,那么这个根据被中断的时机不同返回不同的值。

下面看transferAfterCancelledWait方法。

通过这个方法来决定中断类型。

首先这里设置两个变量,表明不同的中断类型,其中REINTERRUPT表示这设置中断状态,不对外抛中断异常。THROW_IE表示对外抛中断异常。(开始填坑1)

我们根据transferAfterCancelledWait方法来决定哪种方式暴露对外中断类型。

这里我们从线程被await()方法休眠到被signal方法加到等待队列的过程中,它处的不同状态时候都有可能被中断。总共有四种情况。

1.在条件队列中的时候被中断。也就是再被调用doSignal之前。这是节点的状态为CONDITION

2.在doSignal方法中,先在条件队列中删除这个节点,然后再调用transferForSignal方法,在这个方法的

前被中断。这个时候节点不在条件队列中,也不在等待队列中。状态为CONDITION

3.节点被从条件队列删除了,状态也被更改为0了,但是还没有入队到等待队列中。也就是还没执行enq方法。被中断

4.节点在等待队列中。被中断。

下面通过这个四种情况再结合transferAfterCancelledWait方法看。

1.前面有个if语句,判断能不能把节点状态从CONDTION改成0。啥时候节点状态是CONDITON,就是我上述说的情况1,2。在这种情况下,我们把它入队到等待队列中,并返回true,也就是THROW_IE.在这里对于情况1,如果被入队到等待队列中,因为它还没有从条件队列中删除,所以这是这个节点即在条件队列中,也在等待队列中。

2.如果状态不是CONDITION,说明是情况3,4,这里用到while语句,就是用来判断情况3的,如果没有入队,那就线程等待会,让它入队。这个时候返回的就是false,也就是REINTERRUPT。

回归到await方法。checkInterruptWhileWaiting我们通过线程是否是被中断唤醒的,来判断来决定是否跳槽循环。如果是被中断的,那就跳出循环往下执行。在这里的时候被唤醒的节点不管是被正常唤醒还是被中断唤醒,都在等待队列中了。然后我们通过acquireQueued方法在等待队列中排队获取线程,如果在等待队列中获取锁了,那就往下执行,如果没有,那么在等待队列中会接着被挂起。然后到interruptMode != THROW_IE方法,执行到这说明两个问题,一个是这个之前被await的线程已经再次获取到锁了,二是acquireQueued返回true,也就是说在等待队列过程中被中断唤醒过,也就是对应着情况4,这个时候设置中断类型为REINTERRUPT。

往下看,这个有个这个方法,

在正常情况下,条件队列的节点都是现在条件队列中删除,然后入队到等待队列中,但是在一种情况下例外,就是我说的情况1,所以这个时候调用下这个方法判断下,如果成立,那么得把这个节点从条件队列删除。

然后看这个方法

这个方法就是根据中断类型的不同觉得不同的处理方式,如果是THROW_IE,就抛异常,如果是REINTERRUPT,通过

方法设置中断状态为中断过。其实总结起来就是如果是在条件队列中被中断了,最后会抛出一个中断异常,如果是在等待队列中被中断了,最后会设置线程中断状态位为已被中断。

猜你喜欢

转载自blog.csdn.net/wang__wang666/article/details/81780468