详解-Condition-底层原理

1,693 阅读11分钟

前言:

在了解 Condition 的源码之前,你需要对 AQS 的源码有一定的认识,因为 Condition 的功能是建立在 AQS 的基础之上,我的上篇文章对 AQS 进行了源码分析。

Condition 的基本使用

Condition是一个多线程协调通信的工具类,可以让某些线程一起等待某个条件(condition),只有满足条件时,线程才会被唤醒。

  • 案例演示

当调用 await方法后,当前线程会释放锁并等待,而其他线程调用 condition 对象的 signal 或者 signalall 方法通知并被阻塞的线程,然后自己执行unlock释放锁,被唤醒的线程获得之前的锁继续执行,最后释放锁。

所以,condition 中两个最重要的方法,一个是 await,一个是signal方法

  1. await:把当前线程阻塞挂起
  2. signal:唤醒阻塞的线程

可以发现 Condition 和我们的 synchronized 的wait/notify 方法很像,所以我们可以了解到 Condition 这个类就是 J.U.C 用来实现线程的主动阻塞和唤醒功能。

Condition 源码分析

  • 看一下 ConditionObject 的结构,只有两个属性,组成了一个单向链表
  1. firstWaiter 指向队列中的头结点
  2. lastWaiter 指向队列中的尾节点

  • 这里有个很重要的点,ConditionObject是 AQS 的内部类,所以它自动有着外部类的引用,因为他是根据 ReentrantLock 对象中的 sync 创建的,而 sync 继承了 AQS 类,所以他和 ReentrantLock 对象共享一个 AQS 队列。

首先调用Condition,需要获得Lock锁,所以意味着会存在一个AQS同步队列,在上面那个案例中,两个线程同时运行,那么AQS的队列可能是下面这种情况,至于为什么请看我的上篇文章,后续已经讲过的细节我会直接跳过。 然后调用了 condition.await() 方法

  • awai() 源码分析

调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了 Condition 相关联的锁

	public final void await() throws InterruptedException {
    		//表示 await 允许被中断
            if (Thread.interrupted())
                throw new InterruptedException();
            //创建一个新的节点,节点状态为 condition,采用的数据结构仍然是链表 
            Node node = addConditionWaiter();
            //释放当前的锁,得到锁的状态,并唤醒 AQS 队列中的一个线程   
            long savedState = fullyRelease(node);
            //如果当前节点没有在同步队列上,即还没有被 signal,则将当前线程阻塞
            int interruptMode = 0;            
            //判断这个节点是否在 AQS 队列上,第一次判断的是 false,因为前面已经释放锁了 
            while (!isOnSyncQueue(node)) {
            	//阻塞线程
                LockSupport.park(this);
                //当线程醒来后,会判断他到底是被 single唤醒的,还是通过中断操作被唤醒的
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                	//如果是通过single唤醒的,结束循环,如果通过中断操作唤醒的,则继续阻塞,添加进 AQS 队列
                    break;
            }
             // 当这个线程醒来,会尝试拿锁, 当 acquireQueued 返回 false 就是拿到锁了. 
    		 // interruptMode != THROW_IE -> 表示这个线程没有成功将 node 入队,但 signal 执行了 enq 方法让其入队了. 
		     // 将这个变量设置成 REINTERRUPT. 
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            //如果 node 的下一个等待者不是 null, 则进行清理,清理 Condition 队列上的节点.  
			//如果是 null ,就没有什么好清理的了. 
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            //如果线程被中断了,需要抛出异常.或者什么都不做 
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
  • 第一个重要的方法 addConditionWaiter()这个方法的主要作用是把当前线程封装成 Node,添加到 Condition 中的等待队列。这里的队列不再是双向链表,而是单向链表。
	private Node addConditionWaiter() {
    		//获取 lastWaiter指向的 Node 节点
            Node t = lastWaiter;
            //如果lastWaiter不等于空 && 节点的状态不等于 CONDITION 时,把lastWaiter指向的节点删除,然后重新获取 lastWaiter 指向的节点
            if (t != null && t.waitStatus != Node.CONDITION) {
            	//删除 lastWaiter 指向的节点,并且更新 lastWaiter 指向的节点
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            //为当前线程创建一个 Node 节点,并将它的状态设置为 CONDITION(-2) 
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            //如果 lastWaiter 指向的Node的节点为空,firstWaiter指向当前线程的节点
            if (t == null)
                firstWaiter = node;
            //如果不为空 更新 lastWaiter 指向当前线程的 Node 节点
            else
                t.nextWaiter = node;
            //更新 lastWaiter 指向的节点
            lastWaiter = node;
            //返回当前线程的节点
            return node;
        }

执行完addConditionWaiter这个方法之后,就会产生一个这样的condition队列

  • fullRelease,就是彻底的释放锁,什么叫彻底呢,就是如果当前锁存在多次重入,那么在这个方法中只需要释放一次就会把所有的重入次数归零。
    final long fullyRelease(Node node) {
    	//定义 failed 标识
        boolean failed = true;
        try {
       	    //获得重入的次数 
            long savedState = getState();
            //释放锁并且唤醒下一个同步队列中处于阻塞状态的线程 
            if (release(savedState)) {
            	//修改 failed 标识
                failed = false;
                //返回重入的次数
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
        	//如果唤醒线程失败,则修改当前线程节点的状态为 CANCELLED(1)
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }
  • 此时,同步队列会触发锁的释放和重新竞争。ThreadB 获得了锁。

  • isOnSyncQueue() 这个方法比较麻烦
	final boolean isOnSyncQueue(Node node) {
    	//判断当前线程的节点状态是否为 CONDITION || 他的前一个节点是否null
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        //判断他的下一个节点
        if (node.next != null) 
            return true;
        //如果在 AQS 队列中找到了当前线程的节点返回true,否则返回false
        return findNodeFromTail(node);
    }

判断当前节点是否在同步队列中,返回false表示不在,返回true表示在

如果不在AQS同步队列,说明当前节点没有唤醒去争抢同步锁,所以需要把当前线程阻塞起来,直到其他的线程调用signal唤醒。

如果在AQS同步队列,意味着它需要去竞争同步锁去获得执行程序执行权限

为什么要做这个判断呢?原因是在 condition 队列中的节点会重新加入到AQS队列去竞争锁。也就是当调用 signal 的时候,会把当前节点从 condition 队列转移到 AQS 队列

判断 ThreadA 这个节点是否存在于AQS队列中

  1. 如果ThreadA的waitStatus的状态为CONDITION,说明它存在于 condition 队列中,不在 AQS 队列。因为 AQS 队列的状态一定不可能有CONDITION。
  2. 如果node.prev为空,说明也不存在于AQS队列,原因是 prev=null 在 AQS 队列中只有一种可能性,就是它是 head 节点,head节点意味着它是获得锁的节点。
  3. 如果 node.next 不等于空,说明一定存在于 AQS 队列中,因为只有AQS队列才会存在next和prev的关系。
  4. findNodeFromTail,表示从tail节点往前扫描AQS队列,一旦发现 AQS 队列的节点和当前节点相等,说明节点一定存在于AQS队列中。
  • 此时 threadA 已经被阻塞了,并且 threadB 已经拿到锁了,接着执行 signal() 方法
	 public final void signal() {
     		//先判断当前线程是否获得了锁,这个判断比较简单,直接用获得锁的线程和当前线程相比即可 
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            //拿到 firstWaiter 指向的节点
            Node first = firstWaiter;
            if (first != null)
            	//如果不为空,则把 first节点代表的线程加入到 AQS 队列中
                doSignal(first);
        }
  • .doSignal() 方法:对condition队列中从首部开始的第一个condition状态的节点,执行transferForSignal操作,将node从condition 队列中转换到 AQS 队列中,同时修改 AQS 队列中原先尾节点的状态。
	private void doSignal(Node first) {
            do {
            	//从 condition 队列中删除 first 节点,并且判断 condition 队列是否为空队列
                if ( (firstWaiter = first.nextWaiter) == null)
                	// 将 lastWaiter 节点设置成 null 
                    lastWaiter = null;
                //将 first 的 nextWaiter 属性设置为null
                first.nextWaiter = null;
            //添加进AQS队列
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }
  • transferForSignal()方法,该方法先是 CAS 修改了节点状态,如果成功,就将这个节 点放到 AQS 队列中,然后唤醒这个节点上的线程。此时,那个节点就会在 await 方法中苏醒
    final boolean transferForSignal(Node node) {
    	//更新节点的状态为 0,如果更新失败,只有一种可能就是节点被 CANCELLED 了 
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        //调用 enq() 方法把当前节点添加进 AQS 队列中
        Node p = enq(node);
        //拿到当前线程节点的状态
        int ws = p.waitStatus;
        //如果上一个节点的状态被取消了, 或者尝试设置上一个节点的状态为 SIGNAL 失败了(SIGNAL表示: 他的 next 节点需要停止阻塞)
        //如果成功添加进 AQS 队列,则返回true,取反等于false,结束循环
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        	//唤醒线程
            LockSupport.unpark(node.thread);
        return true;
    }
  • 图解分析

执行完doSignal以后,会把condition队列中的节点转移到aqs队列上,逻辑结构图如下这个时候会判断ThreadA的prev节点也就是head节点的waitStatus,如果大于0或者设置SIGNAL失败,表示节点被设置成了CANCELLED状态。这个时候会唤醒 ThreadA 这个线程。否则就基于AQS队列的机制来唤醒,也就是等到ThreadB释放锁之后来唤醒ThreadA。

  • checkInterruptWhileWaiting() 方法,前面在分析await方法时,线程会被阻塞。而通过signal被唤醒之后又继续回到上次执行的代码 checkInterruptWhileWaiting 这个方法是干嘛呢?就是判断ThreadA在condition队列被塞的过程中,有没有被其他线程触发过中断请求
        private int checkInterruptWhileWaiting(Node node) {
        	//判断在唤醒线程前是否调用了中断方法
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        }
  • 如果当前线程被中断,则调用 transferAfterCancelledWait 方法判断后续的处理应该是 抛出InterruptedException还是重新中断。

  • transferAfterCancelledWait() 方法

    final boolean transferAfterCancelledWait(Node node) {
    //使用 cas 修改节点状态,如果还能修改成功,说明线程被中断时,signal 还没有被调用。
    // 这里有一个知识点,就是线程被唤醒,并不一定是在 java 层面执行了 locksupport.unpark,也可能是调用了线程的 interrupt()方法,这个方法会更新一个中断标识,并且会唤醒处于阻塞状态下的线程。 
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            enq(node);
            //如果 cas 成功,则把 node 添加到 AQS 队列 
            return true;
        }
        //如果 cas失败,则判断当前node是否已经在AQS队列上,如果不在,则让给其他线程执行
	    //当node被触发了signal方法时,node就会被加到AQS队列上
        while (!isOnSyncQueue(node))
        	//循环检测 node 是否已经成功添加到 AQS 队列中。如果没有,则通过yield主动释放CPU时间片, 
            Thread.yield();
        return false;
    }

这里需要注意的地方是,如果第一次CAS失败了,则不能判断当前线程是先进行了中断还是先进行了signal方法的调用,可能是先执行了signal然后中断,也可能是先执行了中断,后执行了signal,当然,这两个操作肯定是发生在CAS之前。这时需要做的就是等待当前线程的node被添加到AQS队列后,也就是enq方法返回后,返回false告诉checkInterruptWhileWaiting方法返回 REINTERRUPT(1),后续进行重新中断。

简单来说,该方法的返回值代表当前线程是否在park的时候被中断唤醒,如果为true表示中断在signal调用之前,signal还未执行,那么这个时候会根据await的语义,在await时遇到中断需要抛出 interruptedException,

返回true就是告诉 checkInterruptWhileWaiting返回THROW_IE(-1)。

如果返回false,否则表示signal已经执行过了,只需要重新响应中断即可

await 和 signal 的总结

我把前面的整个分解的图再通过一张整体的结构图来表 述,线程awaitThread先通过lock.lock()方法获取锁成功 后调用了condition.await方法进入等待队列,而另一个线程signalThread通过lock.lock()方法获取锁成功后调用了condition.signal或者signalAll方法,使得线程 awaitThread 能够有机会移入到同步队列中,当其他线程释放lock后使得线程awaitThread能够有机会获取 lock,从而使得线程awaitThread能够从await方法中退出执行后续操作。如果awaitThread获取lock失败会直接进入到同步队列。

  1. 阻塞:await()方法中,在线程释放锁资源之后,如果节点不在AQS等待队列,则阻塞当前线程,如果在等待队列,则自旋等待尝试获取锁
  2. 释放:signal()后,节点会从condition队列移动到 AQS 等待队列,然后进入正常锁的获取流程