JUC之六:Condition接口的实现(ReentrantLock.Condition代码解析)


1、前言

一个新设计的出现,总是为了替换现有的略有不足的设计。而Condition接口的出现,是为了代替监视器锁的wait/notify机制,提供更强大的功能。

我们将Object自带的wait/notify方法与Condition接口提供的await/signal进行一个对比。

Object方法Condition方法区别
void wait()void await()
void wait(long timeout)long awaitNanos(long nanosTimeout)时间单位:前者毫秒ms,后者纳秒ns 返回值
void wait(long timeout, int nanos)boolean await(long time, TimeUnit unit)时间单位:前者只能纳秒,后者什么单位都可以 返回值
void notify()void signal()
void notifyAll()void signalAll()
-void awaitUninterruptibly()Condition独有
-boolean awaitUntil(Date deadline)Condition独有
  • 调用wait()的线程必须已经处于同步代码块中,换言之,调用wait()的线程已经获得了监视器锁;调用await()的线程则必须是已经获得了lock锁

  • 执行wait()时,当前线程会释放已获得的监视器锁,进入到该监视器的等待队列中;执行await()时,当前线程会释放已获得的lock锁,然后进入到该Condition的条件队列中。

  • 退出wait()时,当前线程又重新获得了监视器锁;退出await()时,当前线程又重新获得了lock锁。

  • 调用监视器的notify,会唤醒等待在该监视器上的线程,这个线程此后才重新开始锁竞争,竞争成功后,会从wait方法处恢复执行;调用Condition的signal,会唤醒等待在该Condition上的线程,这个线程此后才重新开始锁竞争,竞争成功后,会从await方法处恢复执行

2、同步队列 和 条件队列

对于每个Condition对象来说,都对应到一个条件队列condition queue。而对于每个Lock对象来说,都对应到一个同步队列sync queue。

2.1sync queue

独占锁的获取过程中,我们提到了,每个线程在lock()尝试获取锁失败后,都会被包装成一个node放到sync queue中去。

在这里插入图片描述

sync queue是一个双向链表,它使用prev和next作为链接。在这个队列中,我们几乎不关心节点的nextWaiter成员,最多会在共享锁模式下,用来标识节点是否为共享锁节点。队头是一个dummy node即Thread成员为null,第一个等待线程永远只能是head的后继。

2.2、condition queue

每一个Condition对象都对应到一个条件队列condition queue,而每个线程在执行await()后,都会被包装成一个node放到condition queue中去。

在这里插入图片描述

condition queue是一个单向链表,它使用nextWaiter作为链接。这个队列中,不存在dummy node,每个节点都代表一个线程。这个队列的节点的状态,我们只关心状态是否为CONDITION,如果是CONDITION的,说明线程还等待在这个Condition对象上;如果不是CONDITION的,说明这个节点已经前往sync queue了。

2.3、condition queue与sync queue关系

本来队列是没有任何关系的,但是如果调用了signal,则会将condition队列上的节点转移到sync上。

在这里插入图片描述

上图简单体现了节点从从condition queue转移到sync queue上去的过程。即使是调用signalAll时,节点也是一个一个转移过去的,因为每个节点都需要重新建立sync queue的链接(enq方法)。

  • 如果一个节点刚入队sync queue,说明这个节点的代表线程没有获得锁(尝试获得锁失败了)。

  • 如果一个节点刚出队sync queue(指该节点的代表线程不在同步队列中的任何节点上,因为它已经跑到了AQS的exclusiveOwnerThread成员上去了),说明这个节点的代表线程刚获得了锁(尝试获得锁成功了)。

  • 如果一个节点刚入队condition queue,说明这个节点的代表线程此时是有锁了,但即将释放。

  • 如果一个节点刚出队condition queue,因为前往的是sync queue,说明这个节点的代表线程此时是没有获得锁的。

​ 在condition队列中的获取锁与释放锁,并没有真正的执行到我们的代码中,只有通过acquireQueued获取到锁才会真正的执行到我们的代码中即lock或await调用处

3、CondtionObject

对于ReentrantLock来说,我们使用newCondition方法来获得Condition接口的实现,而ConditionObject就是一个实现了Condition接口的类。

//ReentrantLock.java
public class ReentrantLock implements Lock, java.io.Serializable {
    public Condition newCondition() {
        return sync.newCondition();
    }

	abstract static class Sync extends AbstractQueuedSynchronizer {
        final ConditionObject newCondition() {
            return new ConditionObject();
        }
    }
}

而ConditionObject又是AQS的一个成员内部类,这意味着不管生成了多少个ConditionObject,它们都持有同一个AQS对象的引用,这和“一个Lock可以对应到多个Condition”相吻合。这也意味着:对于同一个AQS来说,只存在一个同步队列sync queue,但可以存在多个条件队列condition queue。

成员内部类有一个好处,不管哪个ConditionObject对象都可以调到同一个外部类AQS对象的方法上去。比如acquireQueued方法,这样,不管node在哪个condition queue上,最终它们离开后将要前往的地方总是同一个sync queue。

public abstract class AbstractQueuedSynchronizer{
	private transient volatile Node head;
	private transient volatile Node tail;
	
	public class ConditionObject implements Condition {
        private transient Node firstWaiter;
        private transient Node lastWaiter;
	}
}

  • firstWaiter和lastWaiter分别代表条件队列的队头和队尾
  • 注意,firstWaiter和lastWaiter都不再需要加volatile来保证可见性了。这是因为源码作者是考虑,使用者肯定是以获得锁的前提下来调用await() / signal()这些方法的,既然有了这个前提,那么对firstWaiter的读写肯定是无竞争的,既然没有竞争也就不需要 CAS+volatile 来实现一个乐观锁了。

但是这是最理想的状态,若我们没有获取到锁直接调用await(),同样会抛出异常,但这种异常是因为用户使用不规范造成的。

3.1、await方法解析

 public final void await() throws InterruptedException {
     		1、调用await之前就已经中断线程,抛出异常
            if (Thread.interrupted())
                throw new InterruptedException();
     		2、添加到Condition队列中,注意这时线程还是持有锁的
            Node node = addConditionWaiter();
     		3、释放锁,不管当前线程重入锁多少次,都要释放干净,这时线程已经不持有锁了
            int savedState = fullyRelease(node);
     		4、中断状态 0 为中断 1:在合适的地方中断过 -1:在不合适的地方中断过,抛出异常
            int interruptMode = 0;
     		5、是否还在sync 队列中,不是则会park
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                5.1、检查是否中断过,若中断过直接退出循环,就算不在这儿退出循环,也会被正常siganl唤醒执行enq方法加入到sync队列中,同样会退出循环
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
     		6、执行acquireQueued会重新进入sync队列,若执行完acquireQueued说明该线程又重新持有锁了
                判断interruptMode是否等于THROW_IE
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
     		7、node.nextWaiter!=null 只有中断唤醒线程才会成立,这时waitStatus为0,
                但是还在Condintion队列中,来一次大清洗,清除不是Condition的节点
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
     		8、interruptMode根据状态判断是否抛出异常或自我中断
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

注释介绍了大概的过程,但首先要明确会有哪些线程在执行:

  • 执行await的当前线程。这个线程是最开始调用await的线程,也是执行await所有调用链的线程,它被包装进局部变量node中。(后面会以node线程来称呼它)
  • 执行signal的线程。这个线程会改变await当前线程的node的状态state,使得await当前线程的node前往同步队列,并在一定条件在唤醒await当前线程。
  • 中断await当前线程的线程。你就当这个线程只是用来唤醒await当前线程,并改变其中断状态。只不过await当前线程它自己被唤醒后,也会做和上一条同样的事情:“使得await当前线程的node前往同步队列”。
  • 执行unlock的线程。如果await当前线程的node已经是同步队列的head后继,那么获得独占锁的线程在释放锁时,就会唤醒 await当前线程。

理解了这几个线程的存在,对于本文的理解有很大帮助。从用户角度来说,执行await \ signal \ unlock前提都是线程必须已经获得了锁

3.2、addConditionWaiter

        private Node addConditionWaiter() {
            Node t = lastWaiter;//获得队尾
            // 同步队列中的节点只是CONDITION的,就认为是以后将离开条件队列的节点。
            // 将调用unlinkCancelledWaiters来一次大清理,并重新获得队尾
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            // 包装当前线程为node,状态初始为CONDITION
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)// 如果当前队尾为null,那么整个条件队列都没初始化呢
                firstWaiter = node; //把新建node作为队头
            else // 如果队列有至少一个节点
                t.nextWaiter = node;  // 把新建node接在队尾后面
            lastWaiter = node; //让node成为新队尾
            return node;
        }

上面函数描述了新建node加入到条件队列中的过程,我们和独占锁获取过程中新建node加入同步队列进行对比:

  • 同步队列sync queue的新建node,它的初始状态为0。而条件队列condition queue的新建node的初始状态为CONDITION
  • sync queue如果拥有队头,队头肯定会是一个dummy node(即线程成员为null)。condition queue则不会有一个dummy node,每个节点的线程成员都不为null。
  • sync queue是一个双向链表,需要维持前驱和后继都正确。condition queue只是一个单链表,只需要维持后继即可。
  • 这里先提前说下,中断await当前线程的线程(这里特指中断操作在signal之前)和执行signal的线程都会使得条件队列上的node的状态从CONDITION变成0。

3.3、unlinkCancelledWaiters

        private void unlinkCancelledWaiters() {
            Node t = firstWaiter; //获得队头
            Node trail = null; //trail用来保存遍历过程中,最近一次发现的状态为CONDITION的节点
            while (t != null) { //只要循环变量不为null,循环继续
                Node next = t.nextWaiter; //得到循环变量的后继,循环结束前使用
                // 如果循环变量不为CONDITION
                if (t.waitStatus != Node.CONDITION) {
                    t.nextWaiter = null;  //首先使得t与后继断开链接
                    // 如果直到当前循环都还没有发现一个CONDITION的节点
                    if (trail == null)
                        firstWaiter = next;//那么将循环变量的后继,作为新队头
                    // 如果当前循环之前已经发现了一个CONDITION的节点
                    else
                        trail.nextWaiter = next;//那么将trail与next相连,相当于跳过了循环变量t
                    // 如果已经遍历到队尾,需要将trail作为队尾,因为trail才是队列中最后一个为CONDITION的节点
                    if (next == null)
                        lastWaiter = trail;
                }
                // 如果循环变量为CONDITION,则更新trail
                else
                    trail = t;
                t = next;  //循环结束前,用next更新循环变量
            }
        }

主要是一些单链表的操作,trail变量很重要,它用来保存遍历过程中,最近一次发现的状态为CONDITION的节点。

3.4、fullyRelease

在调用fullyRelease之前,当前线程已经被包装成node放到条件队列中去了。注意,在这个函数以后,我们再也不会对firstWaiterlastWaiter轻举妄动了,因为await()以后的执行过程中,当前线程很长一段时间内是没有持有锁的

    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();//这里会获得重入锁的总次数
            if (release(savedState)) {
                failed = false;
                return savedState;// 返回重入锁的总次数
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

该方法比较简单,就是无论重入几次,一次性释放清楚,并返回释放了几次,在后续进入sync队列的时候会在设置上重入锁state的值。

3.5、isOnSyncQueue

   final boolean isOnSyncQueue(Node node) {
		1、第一个条件成了直接返回false,不在sync队列
         2false||true,因当前线程还没获取到锁,必然在sync队列中,
            但prev为null则说明条件不成立,直接返回false
         3false||false。无法判断是否在sync队列中
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
         4、node.next!=null,因为prev不为null,且next不为null,必然在sync队列
        if (node.next != null) // If has successor, it must be on queue
            return true;
		5、执行到这儿,node不为CONDITION,prev不为null,但next为null
            这时要么node是sync队尾;要么是通过cas对tail执行失败,从队尾判断若与node相等则返回true
        return findNodeFromTail(node);
    }


    private boolean findNodeFromTail(Node node) {
        Node t = tail;
        for (;;) {
            if (t == node)
                return true;
            if (t == null)
                return false;
            t = t.prev;
        }
    }

关于状态不为CONDITION,前面有说过,有两种线程可以使得条件队列上的node的状态从CONDITION变成0(中断线程或signal线程)。但现在可以排除中断await当前线程的线程这种情况(之后具体介绍这个流程),因为如果发生了中断,await的while循环直接就会break出循环了(if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break;),也就不会执行到isOnSyncQueue函数了。

简单的说,isOnSyncQueue判断节点已经入队同步队列的标准,必须是node已经成为队尾(包括当前是队尾,或者曾经是队尾)。

3.6、await()第一次调用park之后

这时会阻塞在这儿,需要其他线程signal才会继续执行。

此时node的状态为:

  • 在数据结构上,node在条件队列上(addConditionWaiter)。
  • 在执行过程上,node线程当前阻塞在LockSupport.park(this)这里。

3.7、signalAll

        public final void signalAll() {
            1、判断是否持有锁的线程调用signal的
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            2、获取第一个线程
            Node first = firstWaiter;
            if (first != null)
                doSignalAll(first);
        }


        private void doSignalAll(Node first) {
            1、直接设置为null(注意signal与中断线程退出park的区别)
            lastWaiter = firstWaiter = null;
            do {
                Node next = first.nextWaiter;
                2、设置nextWaiter为null(注意signal与中断线程退出park的区别)
                first.nextWaiter = null;
                3、转移node,从condition队列转移到sync队列中(调用enq方法)
                transferForSignal(first);
                4、循环遍历所有的节点
                first = next;
            } while (first != null);
        }

上来就把代表条件队列的队头队尾成员置null,之后别人就无法通过队头队尾找到队列中的节点了,只有当前线程能通过局部变量first来找到队列节点了。

而接下来不断遍历,直到已经遍历到队尾(first != null)。每次遍历中,将当前遍历节点 与 剩下的条件队列链 断开,然后对当前遍历节点执行transferForSignal。

    final boolean transferForSignal(Node node) {
		1、设置node.waitStatus状态为0.若失败说明有其他线程已经调用了,无论失败与否,都会变为0
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
		2、进入sync队列,放到队尾,剩下的交给独占锁的获取与释放
            注意返回的p是node加入之前的队尾,也就是sync队列倒数第二个节点
        Node p = enq(node);
        int ws = p.waitStatus;
        3、p取消了线程或设置SIGNAL失败,则唤醒node,这时会从whil循环中park继续执行,最终执行到acquireQueued
            把p的状态修改为SIGNAL,这儿的作用其实就是要把node的前驱设置为SIGNAL
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }
  • 如果CAS设置node状态从CONDITION变成0失败了,说明node代表线程因中断而已经执行了中断流程中的compareAndSetWaitStatus(node, Node.CONDITION, 0)。这也间接说明了signal流程和中断流程都是以 成功设置node状态 作为标准,哪个流程成功了,哪个流程就把node入队同步队列,从而以免重复入队。(这一点需要和后面的中断流程内容联动,如果难以理解,可以直接往下看)
  • 如果CAS设置成功,那么enq(node)入队,然后肯定返回true。但是注意,在一定条件下,会唤醒node代表线程。注意enq(node)返回node入队后的前驱prev
    • 这个一定条件是指,node的前驱状态是同步队列节点的取消状态,或者状态<=0但CAS设置前驱状态为SIGNAL失败了。
    • 如果上面条件发生了,就直接唤醒node线程。这里我们回想一下await()第一次调用park之后这个时间点,现在node线程终于被唤醒,假设没有中断发生过的话,不会因break退出循环,再一次检测!isOnSyncQueue(node)会发生条件不成立(node已经因为enq(node)而成功入队)。然后又会走到独占锁获得过程中的acquireQueued函数。
    • 唤醒node代表线程不一定代表它接下来能够获得锁,但是我们也不用担心这会有什么坏影响,因为acquireQueued函数自己会去做判断,如果发现还是获取不到锁的话,则会调用shouldParkAfterFailedAcquire将node的前驱设置为SIGNAL的。
    • 总之,compareAndSetWaitStatus(p, ws, Node.SIGNAL)直接保证了node的前驱状态为SIGNAL,而LockSupport.unpark(node.thread)间接保证了node的前驱状态为SIGNAL,之所以说间接,是因为这不是在signal线程里做的,而是通过唤醒node线程做到的

简单总结一下signalAll方法:

1、将条件队列清空(通过lastWaiter = firstWaiter = null来达到效果,但函数中的局部变量已经保存了队头,且实际上节点的链接还存在着)。
遍历每个节点。
2、如果遍历节点已经被取消掉了(compareAndSetWaitStatus(node, Node.CONDITION, 0)失败),那么直接返回,处理下一个节点。
3、如果遍历节点还没取消掉(compareAndSetWaitStatus(node, Node.CONDITION, 0)成功),那么将其入队同步队列。在一定条件下(无法设置node前驱状态为SIGNAL时),还将唤醒node代表线程。然后处理下一个节点。

另外注意,signalAll方法直到结束返回,都一直没有释放锁呢(因为没有在signalAll里面执行过release),也就是说,执行signalAll的线程一直都是持有锁的

3.8、signal流程

相比signalAllsignal方法只会唤醒一个node,准确的说,是唤醒从同步队列队头开始的第一个状态为CONDITION的node

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);  //与之前的doSignalAll不同,这里是doSignal
}

private void doSignal(Node first) {
    do {
        1、nextWaiter设置为null
        if ( (firstWaiter = first.nextWaiter) == null)
            1.2、若firstWaiter为null,则lastWaiter也设置为null
            lastWaiter = null;
        2、设置nextWaiter为null
        first.nextWaiter = null;
    } while (!transferForSignal(first) && 
             (first = firstWaiter) != null);//transferForSignal失败,设置新的头节点
}

  • 整个逻辑是一个do while循环,不过这个循环一般不会执行多次,只要有一次signal成功了(transferForSignal(first)返回了真)就不会继续循环了,因为这个函数的目的就只是signal一个节点。

  • 如果signal失败了,那么获得新队头((first = firstWaiter) != null),只要新队头不为null,则继续下一次循环。

  • 相比signalAll流程,我们使用的还是这个transferForSignal来做的signal动作,但这里我们终于使用到了transferForSignal的返回值,如果返回了true说明已经signal了一个node了,也就不用继续循环了。
    总之,signal方法会唤醒条件队列中第一个状态不是取消状态(不是CONDITION)的节点。

4、transferForSignal入队与中断线程transferAfterCancelledWait区别

4.1、transferForSignal不唤醒线程

按照上面signal的流程(包括signal和signalAll),假设之前node线程没有被中断过,且执行signalAll的线程不唤醒node线程,那么执行signal流程完毕后此时node的状态为:

  • 在数据结构上,node已经离开了条件队列(first.nextWaiter = null),处于了同步队列上了(Node p = enq(node))。

  • 在执行过程上,node线程当前还是阻塞在await的while循环LockSupport.park(this)这里。(这一点没有变化)

要想node节点执行,则需要该节点的prev节点调用release方法执行(LockSupport.unpark(s.thread);),唤醒node,从await的while循环处的LockSupport.park(this)开始执行,因没有中断,不会通过break退出循环,然后判断循环条件!isOnSyncQueue(node)发现不成立而退出循环,继续执行进入acquireQueued再次尝试获取锁,当然不一定成功获取锁,即使不成功会继续阻塞在acquireQueued方法里的shouldParkAfterFailedAcquire方法处。

4.2、transferForSignal唤醒线程

按照上面signal的流程,假设之前node线程没有被中断过,且执行signalAll的线程唤醒node线程,那么执行signal流程完毕后此时node的状态为:

  • 在数据结构上,node已经离开了条件队列(first.nextWaiter = null),处于了同步队列上了(Node p = enq(node))。

  • 在执行过程上,node线程从LockSupport.park(this)这里被唤醒,不会因为有中断状态而break出循环,然后判断循环条件!isOnSyncQueue(node)发现不成立而退出循环,然后执行acquireQueued。如果不能获得锁,还是会阻塞在acquireQueued的shouldParkAfterFailedAcquire里,若获取锁成功,说明prev为head节点恰好释放了锁。

4.3、中断流程(有线程将node线程中断,在signal之前)

中断await当前线程的线程终于出马了(现在考虑没有执行signal的线程,或者说中断这个node在signal这个node之前),看看中断流程是怎样的流程。首先要知道,中断await当前线程的线程执行完中断动作后,我们就不用关心它了,剩余动作还是靠node线程自己完成的。

本场景下,中断来临之前,node的状态就和 await()第一次调用park之后 一样:

  • 在数据结构上,node在条件队列上(addConditionWaiter)。

  • 在执行过程上,node线程当前阻塞在LockSupport.park(this)这里。

首先,中断来了以后,node线程会从LockSupport.park(this)处被唤醒,然后执行checkInterruptWhileWaiting(之前一直没有讲这个函数,是因为在这个流程中它才会真正发挥作用,之前的signal流程它肯定会返回0的,而返回就不会break出循环)。

        private int checkInterruptWhileWaiting(Node node) {
            //判断贤臣给是否中断过
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        

  • Thread.interrupted()首先判断当前线程有没有被中断过,如果没有,那么返回0.
  • 如果有中断过,那么通过transferAfterCancelledWait(node)判断返回THROW_IE还是REINTERRUPT.

checkInterruptWhileWaiting的返回值最终是会赋值给局部变量interruptMode的,它现在有三种可能值:

1、0:代表整个await过程中没有发生过中断。
THROW_IE:代表await执行完毕返回用户代码处时,需要抛出异常。当中断流程发生在signal流程之前时。
2、REINTERRUPT:代表await执行完毕返回用户代码处时,不需要抛出异常,仅需要重新置上中断状态。当signal流程发生在中断流程之前时。
3、之所以THROW_IE和REINTERRUPT两个值所代表的场景需要进行区分,是因为一个线程A因await而进入condition queue后,正常的流程是另一个线程B执行signal或signalAll后才使得线程A的node入队到sync queue。但如果中断流程发生在signal流程之前,也能使得线程A的node入队到sync queue,但这就没有走正常的流程了。

    final boolean transferAfterCancelledWait(Node node) {
        1、成功则说明中断唤醒发生再signal之前
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            enq(node);
            return true;
        }
		2、等待signal让node入队
        while (!isOnSyncQueue(node))
            Thread.yield();
        return false;
    }

首先通过CAS设置node状态从CONDITION变成0,如果成功了,就将node入队同步队列,然后直接返回true。注意,这里的CAS操作和transferForSignal里的compareAndSetWaitStatus(node, Node.CONDITION, 0)是一样的,也就是说:执行signal的线程 和 因中断被唤醒的node线程,在这句compareAndSetWaitStatus(node, Node.CONDITION, 0)这里有竞争关系,谁竞争赢了,谁才有资格将node入队同步队列enq(node)。

这样的竞争关系是很必要的,直接避免两个线程将同一个node重复入队。

通过CAS设置node状态从CONDITION变成0,如果失败了,那就不能再执行enq(node)啦。因为肯定有另一个signal线程正在执行enq(node)或者已经执行完了enq(node)了。

但这里我们如果发现另一个signal线程还没有执行完了enq(node)(通过!isOnSyncQueue(node)条件判断),就必须一直等待,直到另一个signal线程执行完了enq(node),然后循环才可以退出。之所以这么等一下,是因为如果不等,node线程自己接下来就会执行到acquireQueued了,而执行acquireQueued的前提就是已经入队同步队列完毕。

等到node线程执行完了checkInterruptWhileWaiting,考虑本章的中断场景,就会直接break出循环,然后执行到acquireQueued。如果不能获得锁,还是会阻塞在acquireQueued的shouldParkAfterFailedAcquire里。

总之执行中断流程完毕后此时node的状态为:

  • 在数据结构上,node还是处于条件队列上(没有执行first.nextWaiter = null),但同时也处于了同步队列上了(enq(node))。不用担心nextWaiter会影响到当前node不被独占锁的释放而唤醒,因为独占锁的释放过程不关心head后继的nextWaiter。

  • 在执行过程上,node线程从LockSupport.park(this)这里被中断唤醒,因为有中断状态而break出循环,然后执行acquireQueued。如果不能获得锁,还是会阻塞在acquireQueued的shouldParkAfterFailedAcquire里。

4.4、因中断被唤醒的node线程 和 signal线程 的竞争关系

上面说了中断流程和signal流程谁在前面,await的表现也会有所不同。具体的说,则体现在:因中断被唤醒的node线程 和 signal线程 的竞争关系上。这两个线程完全有可能同时在执行中,而它们的竞争点则体现在:

因中断被唤醒的node线程。transferAfterCancelledWait函数里的compareAndSetWaitStatus(node, Node.CONDITION, 0)。
signal线程。transferForSignal函数里的compareAndSetWaitStatus(node, Node.CONDITION, 0)。
这两个transfer方法都会执行同一个CAS操作,但很明显,只能有一个线程能够执行CAS操作成功。

竞争成功的那一方,transfer方法会返回true,并且会执行enq(node)。
竞争失败的那一方,transfer方法会返回false,并且不会执行enq(node)。
当一个处于条件队列上的node,状态从CONDITION变成0时,就意味着它正在前往同步队列,或者已经放置在同步队列上了。
如果transferAfterCancelledWait竞争成功,我们称这个node线程走的是中断流程。
如果transferForSignal竞争成功,我们称这个node线程走的是signal流程。

5、终于执行到acquireQueued

讲了半天,终于讲到acquireQueued了。但重点内容其实都在前面,acquireQueued后面的都是一些善后处理而已了。

既然已经执行到了acquireQueued,说明又会走独占锁的获取过程了,在此不赘述了。我们只需要知道,从acquireQueued返回时,node线程已经获取到了锁,并且返回了acquireQueued过程中是否有过中断。注意,这和acquireQueued执行前发生的中断是两个不同的中断,acquireQueued执行前发生的中断会被checkInterruptWhileWaiting消耗掉,并赋值给interruptMode的。

        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)  //已经执行到这里啦!!!
                interruptMode = REINTERRUPT; //到这儿说明当前线程再次获取到锁了
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

注意,int savedState = fullyRelease(node)之前释放的同步器状态,现在acquireQueued(node, savedState)都要重新全部再获取回来。

acquireQueued期间发生的中断,重要性不如acquireQueued之前发生的中断。假设acquireQueued发生了中断,acquireQueued(node, savedState)则返回true,然后此时interruptMode有三种情况:

  1. interruptMode为0,说明acquireQueued之前没发生过中断。interruptMode != THROW_IE判断成功。所以需要将interruptMode升级为REINTERRUPT。
  2. interruptMode为REINTERRUPT,说明acquireQueued之前发生过中断(signal流程在中断流程之前的那种)。interruptMode != THROW_IE判断成功。然后将interruptMode从REINTERRUPT变成REINTERRUPT,这好像是脱裤子放屁,但逻辑这样写就简洁了。
  3. interruptMode为THROW_IE,说明acquireQueued之前发生过中断(中断流程在signal流程之前的那种)。interruptMode != THROW_IE判断失败。不会去执行interruptMode = REINTERRUPT,因为执行了反而使得中断等级下降了。说到底,还是因为acquireQueued期间发生的中断,重要性不如acquireQueued之前发生的中断。
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();

当中断唤醒会执行这儿,清楚所有的不是condition的节点。

最后根据interruptMode来判断做出不同的中断反应。

6、await()总结

await()对于使用者来说,进入await()时是持有锁的,阻塞后退出await()时也是持有锁的。signal() signalAll()也是一样。

从实现内部的持有锁情况来看:

  • await()在从开头到fullyRelease执行前,是持有锁的。

  • await()在从fullyRelease执行后 到 acquireQueued执行前,是没有持有锁的。

  • await()在 acquireQueued执行后到最后,是持有锁的。

  • signal() signalAll()全程都是持有锁的。

await()的整体流程如下:

  1. 将当前线程包装成一个node后(Node node = addConditionWaiter()),完全释放锁(int savedState = fullyRelease(node))。
  2. 当前线程阻塞在LockSupport.park(this)处,等待signal线程或者中断线程的到来。
  3. 被唤醒后,到达acquireQueued之前,当前线程的node已经置于sync queue之上了。
  4. 执行acquireQueued,进行阻塞式的抢锁。
  5. 退出acquireQueued时,当前线程已经重新获得了锁,之后进行善后工作。
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值