Condition的源码分析和简单认识

一、开篇

在前面学习 synchronized 的时候,有讲到 wait/notify 的基本使用,结合synchronized 可以实现对线程的通信。那么这个时候我就在思考了,既然 J.U.C 里面提供了锁的实现机制,那 J.U.C 里面有没有提供类似的线程通信的工具呢? 于是我们发现了一个 Condition 工具。
Condition是通过await阻塞,signal/signalALl方法唤醒来工作的,有点类似wait/notify,下面具体通过源码分析Condition的工作原理

二、使用案例

我们以一个生产者-消费者模型来演示
生产者

public class Producer implements Runnable{

    private Queue<String> msg;

    private int maxSize;

    Lock lock;
    Condition condition;

    public Producer(Queue<String> msg, int maxSize, Lock lock, Condition condition) {
        this.msg = msg;
        this.maxSize = maxSize;
        this.lock = lock;
        this.condition = condition;
    }

    @Override
    public void run() {
        int i=0;
        while(true){
            i++;
            lock.lock();
                while(msg.size()==maxSize){
                    System.out.println("生产者队列满了,先等待");
                    try {
                        condition.await(); //阻塞线程并释放锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("生产消息:"+i);
                msg.add("生产者的消息内容"+i);
                condition.signal(); //唤醒阻塞状态下的线程
            lock.unlock();
        }
    }
}

消费者

public class Consumer implements Runnable{
    private Queue<String> msg;

    private int maxSize;

    Lock lock;
    Condition condition;

    public Consumer(Queue<String> msg, int maxSize, Lock lock, Condition condition) {
        this.msg = msg;
        this.maxSize = maxSize;
        this.lock = lock;
        this.condition = condition;
    }

    @Override
    public void run() {
        int i=0;
        while(true){
            i++;
            lock.lock(); //synchronized
            while(msg.isEmpty()){
                System.out.println("消费者队列空了,先等待");
                try {
                    condition.await(); //阻塞线程并释放锁   wait
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("消费消息:"+msg.remove());
            condition.signal(); //唤醒阻塞状态下的线程
            lock.unlock();
        }
    }
}

测试

public class App {
    public static void main( String[] args )
    {
        Queue<String> queue=new LinkedList<>();
        Lock lock=new ReentrantLock(); //重入锁
        Condition condition=lock.newCondition();
        int maxSize=5;

        Producer producer=new Producer(queue,maxSize,lock,condition);
        Consumer consumer=new Consumer(queue,maxSize,lock,condition);

        Thread t1=new Thread(producer);
        Thread t2=new Thread(consumer);
        t1.start();
        t2.start();

    }
}

循环打印效果

生产消息:1
消费消息:生产者的消息内容1
消费者队列空了,先等待
生产消息:2
消费消息:生产者的消息内容2
消费者队列空了,先等待
生产消息:3
消费消息:生产者的消息内容3
生产消息:4
消费消息:生产者的消息内容4
生产消息:5
消费消息:生产者的消息内容5

三、await方法源码分析

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            // //创建一个新的节点,节点状态为 condition,采用的数据结构仍然是链表
            Node node = addConditionWaiter();
            //释放当前的锁,得到锁的状态(锁的重入次数),并唤醒 AQS 队列中的一个线程
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            //如果当前节点没有在同步队列上,即还没有被 signal,则将当前线程阻塞
            //判断这个节点是否在 AQS 队列上,第一次判断的是 false,因为前面已经释放锁了
            while (!isOnSyncQueue(node)) {
            // 第一次总是 park 自己,开始阻塞等待
                LockSupport.park(this);
                // 等到被signal,会从这里继续运行,线程判断自己在等待过程中是否被中断了,如果没有中断,则再次循环,会在 isOnSyncQueue 中判断自己是否在队列上
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 当这个线程醒来,会尝试拿锁, 当 acquireQueued 返回 false 就是拿到锁了.
			// interruptMode != THROW_IE -> 表示这个线程没有成功将 node 入队,但 signal 执行了 enq 方法让其入队了.
			// 将这个变量设置成 REINTERRUPT.
			// acquireQueued方法在AQS分析过,不再赘述
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            // 如果 node 的下一个等待者不是 null, 则进行清理,清理 Condition 队列上的节点.比如那些等待timeOut的节点
			// 如果是 null ,就没有什么好清理的了
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            // 如果线程被中断了,需要抛出异常.或者什么都不做,这一步是对之前判断是否被中断过的响应
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

addConditionWaiter加入到等待队列

private Node addConditionWaiter() {
			// 获取lastWaiter节点
            Node t = lastWaiter;
            // 如果lastWaiter节点不是null,并且节点状态不是CONDITION,就重构链表,删除里面是Cancelled状态的节点
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                // 删除Cancelled状态的节点后,重新获取lastWaiter节点
                t = lastWaiter;
            }
            // 把当前线程封装成一个Node,状态是CONDITION
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            // lastWaiter为null,表示链表为空
            if (t == null)
            	// 链表为空,把当前node赋值给firstWaiter
                firstWaiter = node;
            else
            	// 链表不为空,把lastWaiter节点的的nextWaiter指针指向当前node(其实就是把当前node放到最后一个节点后面)
                t.nextWaiter = node;
             // 把lastWaiter改为当前node
            lastWaiter = node;
            return node;
        }

执行完 addConditionWaiter 这个方法之后,就会产生一个
这样的 condition 队列
Condition的源码分析和简单认识_第1张图片
fullyRelease() 释放锁

final int fullyRelease(Node node) {
        boolean failed = true;
        try {
        	// 锁是重入的
        	// 获取到当前的锁的次数,state=0表示无锁,state=1表示加锁1次,state=3表示加锁3次
            int savedState = getState();
            // release是释放锁的方法,在ReentrenLock源码分析过,一次性全部释放锁,成功返回重入锁次数,失败的话抛出异常,并且把当前节点状态改成Cancelled
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

此时,同步队列会触发锁的释放和重新竞争。ThreadB 获得了锁。
Condition的源码分析和简单认识_第2张图片
isOnSyncQueue判断是否在AQS队列里面

final boolean isOnSyncQueue(Node node) {
		// 此处判断很巧妙,如果节点状态是CONDITION,肯定是不在AQS队列的,或者如果prev==null,要么不在队列上,要么是获取锁的队列,但是刚才释放锁了,说明也不在AQS队列上
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        // 下个节点不是null说明肯定在AQS上,因为等待队列用的是nextWaiter
        if (node.next != null) // If has successor, it must be on queue
            return true;
       // 其他情况下就从后往前找,看是否在AQS队列上
        return findNodeFromTail(node);
    }

findNodeFromTail从AQS队列tail节点往前找,看当前节点是否在队列里

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

checkInterruptWhileWaiting判断线程等待期间是否中断,这个方法注意一下
1.如果在唤醒之前被中断,则返回THROW_IE
2.如果在唤醒之后被中断,则返回REINTERRUPT
3.没被中断,就返回0

如果当前线程被中断,则调用transferAfterCancelledWait 方法判断后续的处理应该是抛出 InterruptedException 还是重新中断。
这里需要注意的地方是,如果第一次 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 已经执行过了,只需要重新响应中断即可

private int checkInterruptWhileWaiting(Node node) {
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        }

transferAfterCancelledWait方法,从方法名称可以看出,这个方法是在调用signal方法后,从等待队列将等待的线程节点转移到AQS队列里面。

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

四、signal源码分析

signal方法

		public final void signal() {
		//先判断当前线程是否获得了锁,这个判断比较简单,直接用获得锁的线程和当前线程相比即可
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
             // 拿到 Condition等待队列上第一个节点
            Node first = firstWaiter;
            // 如果不为null,就调用doSignal唤醒
            if (first != null)
                doSignal(first);
        }

doSignal方法
对condition队列中从首部开始的第一个condition状态的节点,执行 transferForSignal 操作,将 node 从 condition队列中转换到 AQS 队列中,同时修改 AQS 队列中原先尾节点的状态

	private void doSignal(Node first) {
            do {
            //从 Condition 队列中删除 first 节点
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

执行完 doSignal 以后,会把 condition 队列中的节点转移
到 aqs 队列上,逻辑结构图如下
Condition的源码分析和简单认识_第3张图片
这个时候会判断 ThreadA 的 prev 节点也就是 head 节点的 waitStatus,如果大于 0 或者设置 SIGNAL 失败,表示节点被设置成了 CANCELLED 状态。这个时候会唤醒ThreadA 这个线程。否则就基于 AQS 队列的机制来唤醒,也就是等到 ThreadB 释放锁之后来唤醒 ThreadA
transferForSignal方法
该方法先是 CAS 修改了节点状态,如果成功,就将这个节点放到 AQS 队列中,然后唤醒这个节点上的线程。此时,那个节点就会在 await 方法中苏醒

	final boolean transferForSignal(Node node) {
	// CAS更新节点的状态为 0,如果更新失败,只有一种可能就是节点被 CANCELLED 了
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        //调用 enq,把当前节点添加到AQS 队列。并且返回返回按当前节点的上一个节点,也就是原tail 节点
        Node p = enq(node);
        int ws = p.waitStatus;
        // 如果上一个节点的状态被取消了, 或者尝试设置上一个节点的状态为 SIGNAL 失败了(SIGNAL 表示: 他的 next节点需要停止阻塞)
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        	// 唤醒节点上的线程.
            LockSupport.unpark(node.thread);
        //如果 node 的 prev 节点已经是signal 状态,那么被阻塞的 ThreadA 的唤醒工作由 AQS 队列来完成
        return true;
    }

五、总结

我把前面的整个分解的图再通过一张整体的结构图来表述,线程 awaitThread 先通过 lock.lock()方法获取锁成功后调用了 condition.await 方法进入等待队列,而另一个线程 signalThread 通过 lock.lock()方法获取锁成功后调用了 condition.signal 或者 signalAll 方法,使得线程awaitThread 能够有机会移入到同步队列中,当其他线程释放 lock 后使得线程 awaitThread 能够有机会获取lock,从而使得线程 awaitThread 能够从 await 方法中退出执行后续操作。如果 awaitThread 获取 lock 失败会直接进入到同步队列。
把我们的生产者-消费者模型抽象如下
Condition的源码分析和简单认识_第4张图片
阻塞:await()方法中,在线程释放锁资源之后,如果节点不在 AQS 等待队列,则阻塞当前线程,如果在等待队列,则自旋等待尝试获取锁
释放:signal()后,节点会从 condition 队列移动到 AQS等待队列,则进入正常锁的获取流程
了解完Lock以及Condition 之后,意味着我们对于J.U.C里面的锁机制以及线程通信机制有了一个全面和深入的了解。

本文是综合自己的认识和参考各类资料(书本及网上资料)编写,若有侵权请联系作者,所有内容仅代表个人认知观点,如有错误,欢迎校正; 邮箱:[email protected] 博客地址:https://blog.csdn.net/qq_35576976/

你可能感兴趣的:(多线程)