实现线程通信的好工具--Condition

一:简述

说到线程通信可能大家第一时间想到的可能是wait()和notify(),而wait()和notify()是在jvm中实现的,condition.await()方法和condition.signal()是Doug Lea老爷子在java.util.concurrent包中提供的实现。今天就给大家介绍condition。

注:因为condition是基于AQS和ReentrantLock来实现的,会涉及到ReentrantLock的lock()方法和unlock()以及AQS中的一些方法,所以强烈建议先去看我的另外一篇文章,了解ReentrantLock以及AQS的实现,然后再来看本篇文章。文章地址:juejin.cn/post/704855…

二:利用condition实现一个生产者消费者模型

public class TestThread {


    static Lock lock = new ReentrantLock();

    static Condition notEmpty = lock.newCondition();

    static Condition notFull = lock.newCondition();

    static Queue<String> queue = new LinkedList<>();

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (true){
                try{
                    lock.lock();
                    if (queue.size() >= 10){
                        System.out.println("队列满了");
                        notFull.await();
                    }
                    System.out.println("生产者生产消息");
                    queue.add("消息");
                    notEmpty.signal();
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }
        }).start();

        Thread.sleep(200);

        new Thread(()->{
            while (true){
                try{
                    lock.lock();
                    if (queue.isEmpty()){
                        System.out.println("队列空了");
                        notEmpty.await();
                    }
                    String poll = queue.poll();
                    System.out.println("消费者消费" + poll);
                    notFull.signal();
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }
        }).start();
    }
}
复制代码

三:原理分析

未命名文件 (1).png

源码分析:

分别对await()方法和signal()方法进行分析

await()方法

首先调用addConditionWaiter()方法创建一个状态为CONDITION的Node 并且将它放入到condition队列(一个单向链表)中的尾部,然后调用fullyRelease()方法释放锁,释放完锁之后调用LockSupport.park()方法阻塞线程,等待被唤醒。

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
	    //创建一个Node节点 并且将节点放入condition队列中	
            Node node = addConditionWaiter();
	    //彻底释放锁 并且将锁的重入次数保存 savedState表示释放锁之前的重入次数,因为如果被signal()唤醒之后抢占到锁 重入次数必须要维持跟原来一样
            int savedState = fullyRelease(node);
            int interruptMode = 0;
	    //判断队列中node节点是否在AQS同步队列中
            while (!isOnSyncQueue(node)) {
		  // 阻塞当前线程 当另外一个线程调用signal()方法时 会把这个线程从condition队列加入到AQS队列中
		 // 这样当其他线程调用unlock()方法时 就有机会被唤醒了。
                LockSupport.park(this);
                //线程被唤醒之后继续执行
		//检查线程在等待的时候是否被中断
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
	    //acquireQueued()方法尝试获取锁 没有获取到就重新加入到AQS队列并且阻塞起来
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
		// 清除取消状态的节点
                unlinkCancelledWaiters();
            if (interruptMode != 0)
		// interruptMode不为0 说明线程被中断过 如果线程被中断过 根据interruptMode 进行不同的处理
                reportInterruptAfterWait(interruptMode);
        }
复制代码

addConditionWaiter()

addConditionWaiter()做两件事情,第一件事把condition队列中状态为取消的节点清除,第二件事就是将当前线程封装成一个状态为CONDITION的Node节点,并且添加到condition队列的末尾。

private Node addConditionWaiter() {
            Node t = lastWaiter;
            // If lastWaiter is cancelled, clean out.
	    //如果节点是取消的状态 那么需要清除这种节点
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
	   // 创建一个CONDITION状态的Node节点
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
	   //如果尾节点为空 那么将condition链表的头结点和尾结点都指向新建的节点 (初始化condition链表)
            if (t == null)
                firstWaiter = node;
            else
		//尾不为空 直接将新节点添加到condition链表尾部
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }
复制代码

unlinkCancelledWaiters()

去除condition队列中状态为cancelled的节点

private void unlinkCancelledWaiters() {
            Node t = firstWaiter;
	    //用来记录前一个合法的节点
            Node trail = null;
	    // 从头节点开始遍历 将不是CONDITION 状态的节点清除 (不是CONDITION 就是cancelled)
            while (t != null) {
                Node next = t.nextWaiter;
                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;
            }
        }
复制代码

接下来看fullyRelease()方法

fullyRelease()

fullyRelease()方法其实就是调用了release()方法,而release()方法的作用就是释放锁并且唤醒将AQS队列中的线程唤醒(它的源码已经在上一篇文章 # ReentrantLock源码分析 中分析了,这里就不重复了,有兴趣的同学可以看一看 文章地址:juejin.cn/post/704855…

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;
        }
    }
复制代码

之后线程会调用LockSupport.park()方法阻塞,线程被LockSupport.park()方法阻塞之后,其他线程就有机会获取到锁,并且调用signal()方法。所以我们先看signal()方法,然后再回头分析线程被唤醒之后的代码。

signal()

condition队列不为空的情况下signal()方法会去调用doSignal()方法

public final void signal() {
            //判断获取锁的线程是否是当前线程 不是抛出异常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            //如果condition队列不为空
            if (first != null)
                //调用doSignal()方法
                doSignal(first);
        }
复制代码

doSignal()

利用do while 循环现将节点从condition队列中移除 然后调用transferForSignal()方法将节点从condition队列迁移到AQS队列中。

private void doSignal(Node first) {
            do {
                //将节点从condition队列中移除
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            //transferForSignal()迁移节点失败而且condition队列不为空 继续循环直到迁移成功。    
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }
复制代码

transferForSignal()

transferForSignal()方法调用enq()方法将节点加入到AQS队列中 (enq()方法源码已经在上一篇文章 # ReentrantLock源码分析 中分析了,这里就不重复了,有兴趣的同学可以看一看 文章地址:juejin.cn/post/704855…

final boolean transferForSignal(Node node) {
        //cas节点状态修改为0 如果修改失败 说明节点状态不是CONDITION 
        //也就是说节点是cancelled状态 在condition队列中只有两种状态 CONDITION 和 cancelled 直接但会false
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        //enq()方法就是构建一个节点加入到AQS队列中 
        Node p = enq(node);
        int ws = p.waitStatus
        //如果节点状态是取消或者cas替换失败 就需要将线程唤醒重新同步
        //注意 这里只是对异常节点的处理 正常的节点并不是在这里被唤醒 而是调用signal()方法的线程 调用unlock()方法后被唤醒  unlock()方法源码就是释放锁并将AQS队列中的节点唤醒。
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }
复制代码

当线程调用signal()方法将节点迁移AQS队列之后,调用unlock()方法会将AQS队列中的节点唤醒 然后继续执行await()方法中没有执行完成的代码,也就是LockSupport.park()之后的代码。线程被唤醒之后会调用acquireQueued()方法, acquireQueued()方法的作用就是尝试去抢占锁,抢占成功之后把节点从AQS队列中移除,并且回复原来的重入次数。最后根据不同的interruptMode对线程的中断进行处理。

所以回到await()方法 继续分析await()被唤醒之后的流程

await()

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
	    //创建一个Node节点 并且将节点放入condition队列中	
            Node node = addConditionWaiter();
	    //彻底释放锁 并且将锁的重入次数保存 savedState表示释放锁之前的重入次数,因为如果被signal()唤醒之后抢占到锁 重入次数必须要维持跟原来一样
            int savedState = fullyRelease(node);
            int interruptMode = 0;
	    //判断队列中node节点是否在AQS同步队列中
            while (!isOnSyncQueue(node)) {
		  // 阻塞当前线程 当另外一个线程调用signal()方法时 会把这个线程从condition队列加入到AQS队列中
		 // 这样当其他线程调用unlock()方法时 就有机会被唤醒了。
                LockSupport.park(this);
                //线程被唤醒之后继续执行 因为signal线程已经把节点加入到AQS队列,所以唤醒之后肯定会跳出while循环
		//检查线程在等待的时候是否被中断
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
	    //acquireQueued()方法尝试获取锁 没有获取到就重新加入到AQS队列并且阻塞起来 注意:获取锁必须保持和之前一样的重入次数
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
		// 清除取消状态的节点
                unlinkCancelledWaiters();
            if (interruptMode != 0)
		// interruptMode不为0 说明线程被中断过 如果线程被中断过 根据interruptMode 进行不同的处理
                reportInterruptAfterWait(interruptMode);
        }
复制代码
private void reportInterruptAfterWait(int interruptMode)
            throws InterruptedException {
            //如果是THROW_IE 抛出异常  如果是 REINTERRUPT 将中断标记修改交给客户端处理
            if (interruptMode == THROW_IE)
                throw new InterruptedException();
            else if (interruptMode == REINTERRUPT)
                selfInterrupt();
        }
复制代码

四:总结

Condition是jdk1.5并发包提供的工具,可以利用它来实现线程之间的通讯,它的实现原理和wait(),notify()是一样的 所以Condition学会之后 wait(),notify()也就明白了,下一次给大家介绍并发包中的工具--阻塞队列,而阻塞队列正是基于Condition来实现。

猜你喜欢

转载自juejin.im/post/7049730925684326436