JUC--Condition源码分析(基于JDK1.8)

1 概述

通过前面的文章(JUC–Condition学习(一)简介和使用),我们对Condition有了一个初步的认识,并且我们也知道了如何使用Condition,现在我们就来看一看Condition到底是如何实现线程的等待和唤醒的。

2 等待队列

在我们学习AQS的时候,我们知道当线程获取锁失败的时候会进入CLH队列,以等待状态(WAITING)存在于同步队列。而当我们调用Condition.await()方法后线程会以等待状态进入等待队列。

其实不管是等待队列还是同步队列都是依靠AQS的内部类Node来建立队列的。

那么等待队列和同步队列又有什么区别呢?我们直接看两张图就明白了。

(1)同步队列图

《JUC--Condition源码分析(基于JDK1.8)》

(2)等待队列图

《JUC--Condition源码分析(基于JDK1.8)》

上面第一张图是同步队列第二张是等待队列,我们可以看出等待队列每个节点仅仅持有指向下一节点的引用,即等待队列是单向链表,而同步队列是双向链表。

3 await函数

调用Condition的await()方法会使当前线程进入等待状态,同时会加入到Condition等待队列同时释放锁。当从await()方法返回时,当前线程一定是获取了Condition相关联的锁。

通过查看源码,我们知道接口Condition的实现都交给了AQS的内部类ConditionObject。所以await函数的实现其实也在ConditionObject里面。

首先我们来看一下该函数的调用顺序。

《JUC--Condition源码分析(基于JDK1.8)》

首先我们来看一下await方法:

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);
				
	        //在等待过程中获取中断状态,如果不为0,则已经线程中断,直接break
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                break;
            }
			
	    //竞争同步状态
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
			
	    //清理条件队列中不是在等待状态的节点
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
	    //针对中断状态不为0的情况,响应中断(要么抛出异常,要么自我中断)
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

方法的基本逻辑在注释中已经有体现了,我们接下来看看加入条件队列是如何实现的,其实这里我们可以猜想加入条件队列(即等待队列)无非就是生成节点,将节点添加到条件队列的尾节点后面,设置等待状态。那么猜想对不对呢?如下:

//将当前线程加入条件队列
private Node addConditionWaiter() {
    Node t = lastWaiter;
            
    //如果尾节点被删除则清除尾节点.
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
			
    //生成节点,并将节点加入条件队列当中
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

从上面可以看出我们的猜想是正确的,无非就是多了一个清除已经删除的节点的操作。那么到底是如何清除已经删除的节点的呢?

//将已经删除的等待节点从条件队列中移除
private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    Node trail = null;
			
    //首节点不为空
    while (t != null) {
        Node next = t.nextWaiter;
				
	//将节点的等待状态不为CONDITION的从条件队列中移除
        if (t.waitStatus != Node.CONDITION) {
           t.nextWaiter = null;
           if (trail == null)
               firstWaiter = next;
           else
               trail.nextWaiter = next;
           if (next == null)
               lastWaiter = trail;
       }
       else
           trail = t;
       t = next;
    }
}

上面我们看了如何将节点加入条件队列,下面我们来看看如何释放节点持有的锁。

//释放节点持有的锁
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;
    }
}

从上面的代码我们可以猜想释放节点的锁最终就是改变同步状态,改变持有锁的线程,唤醒后继节点。

//释放节点持有的锁
public final boolean release(int arg) {
			
    //释放节点持有的锁
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
					
            //唤醒首节点的后继节点
	    unparkSuccessor(h);
	return true;
    }
    return false;
}

await方法判断节点不在同步队列上的时候将阻塞线程,我们来看一看如何判断节点是否在同步队列上:

//判断节点是否在同步队列上
final boolean isOnSyncQueue(Node node) {
	
	//由于当前节点的状态是CONDITION,所有返回false
	if (node.waitStatus == Node.CONDITION || node.prev == null)
		return false;
	if (node.next != null) // If has successor, it must be on queue
		return true;
	
	return findNodeFromTail(node);
}

isOnSyncQueue方法就比较简单了,这里当前的节点是CONDITION状态,所以直接返回false。

在while循环中,我们可以看见当当前节点不在同步队列中就直接挂起当前线程,进行等待。循环直到当前节点在同步队列中的时候才继续后面的执行,针对后面处理中断的其余代码,这里就不继续分析了,比较简单,可以直接查看源码。

4 signal函数

调用Condition的signal()方法,将会唤醒在等待队列中等待最长时间的节点(条件队列里的首节点),在唤醒节点前,会将节点移到CLH同步队列中。

我们依然来看一下signal函数的调用顺序:

《JUC--Condition源码分析(基于JDK1.8)》

我们依次来查看上面方法的源码:

public final void signal() {

    //判断当前线程是否拥有独占锁
    if (!isHeldExclusively())
	throw new IllegalMonitorStateException();
    Node first = firstWaiter;

    //首节点不为空则唤醒首节点
    if (first != null)
	doSignal(first);
}

从上面我们可以看出signal方法的实现主要是依靠doSignal唤醒首节点来实现的。

private void doSignal(Node first) {
    do {

        //修改头节点,完成旧头节点的移除工作
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;

    //循环直到将首节点从条件队列移入同步队列成功或者首节点为空
    } while (!transferForSignal(first) &&(first = firstWaiter) != null);
}

接下来我们看一看到底是怎样将首节点进行转移的。

final boolean transferForSignal(Node node) {
        
        //删除等待状态
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        //添加进入等待同步队列,返回同步队列中的前继节点
        Node p = enq(node);
        int ws = p.waitStatus;

        //如果结点p的状态为cancel 或者修改waitStatus失败,则直接唤醒
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

上面就是针对Condition核心方法的分析,其余的方法原理基本一样,这里就不逐一分析了。

谢谢关注,如有不同看法欢迎指正。

    原文作者:JUC
    原文地址: https://blog.csdn.net/ONROAD0612/article/details/82380230
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞