软件工程

3.1.3 获取队列(节点,int)

好吧,你应该马上等待状态休息一下,然后等待其他状态,然后等待其他任务表面上来后发现自己拿到了资源是肯定的,然后就可以想干想干了。事情:在等闲下来就可以拿走其他事情了)


1 final boolean acquireQueued(final Node node, int arg) {
2 boole failed = true;//标记是否成功获取资源
3尝试{
4 boolean interrupted = false;//标记等待过程中是否被中断过
5
6 //又是一个“自旋”!
7 代表 (;;) {
8 final Node = node.predecessor();//到达前驱
9 //前驱是头,即该结点已成老二,因此有资格去获取资源(可能是老大的释放资源结束自己的,当然也可能被中断了)。
10 if (p == head && tryAcquire(arg)) {
11 setHead(node);//拿到资源后,将头指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。
12 p.next = null; // setHead中node.prev置为null,这里再将资源将head.next置,就是为了方便回收之前的GC头结点为null。意味着之前的结点已经拿完的点出队了!
13 失败 = 假; // 成功获取资源
14返回中断;//返回等待过程中是否被中断
15 }
16
17 //自己休息可以了,就通过park()进入等待状态,直到被unpark()。不可中断的情况如果被下到了,那么会从park()中醒过来,发现拿不到资源,继续进入park()等待。
18 if (shouldParkAfterFailedAcquire(p, node) &&
19parkAndCheckInterrupt())
20中断=真;//如果等待过程中被中断,就只有一次,将中断那么标记为真
21 }
22 } 最后 {
23 if (failed) //等待过程中没有成功获取资源(如超时,或者可以中断的情况下被中断了),那么如果取消结点在中的等待。
24 取消获取(节点);
25 }
26 }



到这里了,我们先不急着总结acquireQueued()的函数,先看看shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()具体干啥。

3.1.3.1 shouldParkAfterFailedAcquire(Node, Node)

主要方法用来检查状态,这个是否真的可以去休息了(进入等待,如果查看线程状态转换不熟),可以自己上一篇文章的线程,万一可以详解的线程状态都放弃只是瞎站着,那也说不定,对吧!


1 私有静态布尔值 shouldParkAfterFailedAcquire(Node pred, Node node) {
2 inws = pred.waitStatus;//t达到前驱的状态
3 if (ws == Node.SIGNAL)
4 //如果已经告诉前驾驶拿完号后通知自己一下,那可以安心休息了
5 返回真;
6 如果 (ws > 0) {
7 /*
8 * 如果前驱放弃了,那就一直往前找,直到最近找到
Loading

3.1.3 acquireQueued(Node, int)

  OK,通过tryAcquire()和addWaiter(),该线程获取资源失败,已经被放入等待队列尾部了。聪明的你立刻应该能想到该线程下一部该干什么了吧:进入等待状态休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源,然后就可以去干自己想干的事了。没错,就是这样!是不是跟医院排队拿号有点相似~~acquireQueued()就是干这件事:在等待队列中排队拿号(中间没其它事干可以休息),直到拿到号后再返回。这个函数非常关键,还是上源码吧:

复制代码
复制代码
 1 final boolean acquireQueued(final Node node, int arg) {
 2     boolean failed = true;//标记是否成功拿到资源
 3     try {
 4         boolean interrupted = false;//标记等待过程中是否被中断过
 5         
 6         //又是一个“自旋”!
 7         for (;;) {
 8             final Node p = node.predecessor();//拿到前驱
 9             //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。
10             if (p == head && tryAcquire(arg)) {
11                 setHead(node);//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。
12                 p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
13                 failed = false; // 成功获取资源
14                 return interrupted;//返回等待过程中是否被中断过
15             }
16             
17             //如果自己可以休息了,就通过park()进入waiting状态,直到被unpark()。如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
18             if (shouldParkAfterFailedAcquire(p, node) &&
19                 parkAndCheckInterrupt())
20                 interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
21         }
22     } finally {
23         if (failed) // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
24             cancelAcquire(node);
25     }
26 }
复制代码
复制代码

  

到这里了,我们先不急着总结acquireQueued()的函数流程,先看看shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()具体干些什么。

3.1.3.1 shouldParkAfterFailedAcquire(Node, Node)

  此方法主要用于检查状态,看看自己是否真的可以去休息了(进入waiting状态,如果线程状态转换不熟,可以参考本人上一篇写的Thread详解),万一队列前边的线程都放弃了只是瞎站着,那也说不定,对吧!

复制代码
复制代码
 1 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
 2     int ws = pred.waitStatus;//拿到前驱的状态
 3     if (ws == Node.SIGNAL)
 4         //如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了
 5         return true;
 6     if (ws > 0) {
 7         /*
 8          * 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
 9          * 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
10          */
11         do {
12             node.prev = pred = pred.prev;
13         } while (pred.waitStatus > 0);
14         pred.next = node;
15     } else {
16          //如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!
17         compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
18     }
19     return false;
20 }
复制代码
复制代码

 

整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,同时可以再尝试下看有没有机会轮到自己拿号。

3.1.3.2 parkAndCheckInterrupt()

  如果线程找好安全休息点后,那就可以安心去休息了。此方法就是让线程去休息,真正进入等待状态。

1 private final boolean parkAndCheckInterrupt() {
2     LockSupport.park(this);//调用park()使线程进入waiting状态
3     return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
4 }

   park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。(再说一句,如果线程状态转换不熟,可以参考本人写的Thread详解)。需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。 

3.1.3.3 小结

  OK,看了shouldParkAfterFailedAcquire()和parkAndCheckInterrupt(),现在让我们再回到acquireQueued(),总结下该函数的具体流程:

  1. 结点进入队尾后,检查状态,找到安全休息点;
  2. 调用park()进入waiting状态,等待unpark()或interrupt()唤醒自己;
  3. 被唤醒后,看自己是不是有资格能拿到号。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程1。

 

3.1.4 小结

  OKOK,acquireQueued()分析完之后,我们接下来再回到acquire()!再贴上它的源码吧:

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

再来总结下它的流程吧:

  1. 调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
  2. 没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
  3. acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

由于此函数是重中之重,我再用流程图总结一下:

至此,acquire()的流程终于算是告一段落了。这也就是ReentrantLock.lock()的流程,不信你去看其lock()源码吧,整个函数就是一条acquire(1)!!!

 

3.2 release(int)

   上一小节已经把acquire()说完了,这一小节就来讲讲它的反操作release()吧。此方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。这也正是unlock()的语义,当然不仅仅只限于unlock()。下面是release()的源码:

复制代码
复制代码
1 public final boolean release(int arg) {
2     if (tryRelease(arg)) {
3         Node h = head;//找到头结点
4         if (h != null && h.waitStatus != 0)
5             unparkSuccessor(h);//唤醒等待队列里的下一个线程
6         return true;
7     }
8     return false;
9 }
复制代码
复制代码

 

  逻辑并不复杂。它调用tryRelease()来释放资源。有一点需要注意的是,它是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自定义同步器在设计tryRelease()的时候要明确这一点!!

3.2.1 tryRelease(int)

  此方法尝试去释放指定量的资源。下面是tryRelease()的源码:

1 protected boolean tryRelease(int arg) {
2     throw new UnsupportedOperationException();
3 }

 

  跟tryAcquire()一样,这个方法是需要独占模式的自定义同步器去实现的。正常来说,tryRelease()都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可(state-=arg),也不需要考虑线程安全的问题。但要注意它的返回值,上面已经提到了,release()是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false。

3.2.2 unparkSuccessor(Node)

  此方法用于唤醒等待队列中下一个线程。下面是源码:

复制代码
复制代码
 1 private void unparkSuccessor(Node node) {
 2     //这里,node一般为当前线程所在的结点。
 3     int ws = node.waitStatus;
 4     if (ws < 0)//置零当前线程所在的结点状态,允许失败。
 5         compareAndSetWaitStatus(node, ws, 0);
 6 
 7     Node s = node.next;//找到下一个需要唤醒的结点s
 8     if (s == null || s.waitStatus > 0) {//如果为空或已取消
 9         s = null;
10         for (Node t = tail; t != null && t != node; t = t.prev) // 从后向前找。
11             if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
12                 s = t;
13     }
14     if (s != null)
15         LockSupport.unpark(s.thread);//唤醒
16 }
复制代码
复制代码

 

  这个函数并不复杂。一句话概括:用unpark()唤醒等待队列中最前边的那个未放弃线程,这里我们也用s来表示吧。此时,再和acquireQueued()联系起来,s被唤醒后,进入if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然s已经是等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,下一次自旋p==head就成立啦),然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了!!And then, DO what you WANT!

 

看到有不少小伙伴问为什么从后向前找,而不是从前往后找?从前往后更简单啊。

好问题!!!从源码上看,先找到后继结点s,如果s状态正常那么直接唤醒。但有两种异常情况,会导致next链不一致:

1)s==null,在新结点入队时可能会出现

posted @   薛定谔的小冰  阅读(40)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
点击右上角即可分享
微信分享提示