Java并发编程|第七篇:Condition

霸气de小男生 提交于 2019-12-18 10:58:21

Java并发编程|第七篇:Condition

Condition 是一个多线程协调通信的工具类,可以让某些线程一起等待某个条件,只有满足条件时,线程才会被唤醒,功能和使用方法可以类比 wait/notify 。

1.例子

wait/notify

我们先结合synchronized 实现一个简单的 wait/notify 的例子

为了测试方便,将Consumer作为WaitNotifyDemo的静态内部类

public class WaitNotifyDemo implements Runnable {

    private Object lock;

    public WaitNotifyDemo(Object lock) {
        this.lock = lock;
    }

    public static void main(String[] args) {
        Object lock = new Object();

        new Thread(new WaitNotifyDemo(lock)).start();

        new Thread(new Consumer(lock)).start();

    }

    @Override
    public void run() {
        synchronized (lock) {
            try {
                System.out.println(Thread.currentThread().getName() + " >>>> begin - wait");
                lock.wait();
                System.out.println(Thread.currentThread().getName() + " >>>> end - wait");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    static class Consumer implements Runnable {

        private Object lock;

        public Consumer(Object lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + " >>>> begin - notify");
                lock.notify();
                System.out.println(Thread.currentThread().getName() + " >>>> end - notify");
            }
        }
    }

}

执行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ioR0i9MY-1576632025117)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20191216173213103.png)]

Condition

将上面的例子改写成一个Condtion的实现

  • Condition 中的 await 相当于wait

    • 阻塞当前线程
  • Condition中 的 single 相当于 notify

    • 唤醒阻塞的线程
  • Condition中 的 signalAll 相当于 notifyAll

    • 唤醒所有阻塞的线程

ConditionWait

public class ConditionWait implements Runnable {

    private Lock lock;
    private Condition condition;

    public ConditionWait(Lock lock, Condition condition) {
        this.lock = lock;
        this.condition = condition;
    }

    @Override
    public void run() {
        try {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " >>> begin - conditionWait");
                condition.await();
                System.out.println(Thread.currentThread().getName() + " >>> end - conditionWait");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            lock.unlock();
        }
    }
    
}

ConditionNotify

public class ConditionNotify implements Runnable {
    private Lock lock;
    private Condition condition;

    public ConditionNotify(Lock lock, Condition condition) {
        this.lock = lock;
        this.condition = condition;
    }

    @Override
    public void run() {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + " >>> begin - conditionNotify");
            condition.signal();//唤醒线程
            System.out.println(Thread.currentThread().getName() + " >>> end - conditionNotify");
        } finally {
            lock.unlock();
        }
    }

}

CondtionDemo

public class CondtionDemo {

    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        ConditionWait conditionWait = new ConditionWait(lock, condition);
        new Thread(conditionWait).start();

        ConditionNotify conditionNotify = new ConditionNotify(lock, condition);
        new Thread(conditionNotify).start();
    }
}

执行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CTIuV9OD-1576632025118)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20191216185242450.png)]

2.源码分析

调用Condition之前,先需要获得Lock锁,以上面的代码为例,一定会存在一个AQS的队列,如果线程A线程B同时运行,AQS的队列可能是如下情况,线程A获取锁,线程B加入到AQS队列,封装成阻塞状态的节点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iDRprsCX-1576632025118)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20191216190919256.png)]

如果此时ThreadA调用await()方法,ThreadA 阻塞并释放锁。

await()

将线程A挂机,并加入到 Condition队列中

        public final void await() throws InterruptedException {
            if (Thread.interrupted())//线程复位,并返回线程是否被中断过
                throw new InterruptedException();
            Node node = addConditionWaiter();//创建一个新的节点状态为Condition(-2)
            int savedState = fullyRelease(node);//彻底释放锁,如果当前锁存在多次重入,在此方法中会把所有的重入次数归零
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {//判断当前节点是否存在AQS队列中
                LockSupport.park(this);//如果不存在AQS,挂起当前线程
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            //竞争锁成功并且没有中断异常
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;//重新中断
            if (node.nextWaiter != null) // 当前节点为Condition队列节点
                unlinkCancelledWaiters();//剔除Condition队列节点
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

addConditionWaiter()

线程A添加节点到Condition队列,Condition队列是一个单向链表

       private Node addConditionWaiter() {
            Node t = lastWaiter;
            // 如果lastWaiter不为空 并且waitStatus不等于-2
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();//把这个节点从链表中移除
                t = lastWaiter;
            }
           //构建一个Condition链表的Node节点状态为-2的
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

线程A执行完之后会形成下面的Condition队列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XWPnAkXN-1576632025118)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20191216193042292.png)]

假设再来一个线程C也执行await()方法,节点会继续往下添加

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TytmvIqI-1576632025119)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20191217171622085.png)]

fullyRelease()

彻底释放锁,如果当前锁存在多次重入,在此方法中会把所有的重入次数归零

    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            //获取当前state>0 表示重入多少次
            int savedState = getState();
            //最终会调用tryRelease 将重入次数归零
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

tryRelease()

通过 getState() - releases 直接使得 c = 0 ,因为releases 传入的是重入次数

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

线程A释放锁,线程B获的锁,此时 AQS 队列如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G3LBVv0n-1576632025119)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20191217184649553.png)]
继续往下走

isOnSyncQueue()

判断当前线程是否存在AQS队列中,通过下面四种方式

1.如果Node节点的waitStatus状态 = -2 表示线程在Condition队列

2.如果Node节点的prev节点为空,那么它只能是head节点,表示它是获得锁的节点,而不是当前释放锁的节点

3.如果Node节点的next节点 不为空,只有AQS队列才存在 nextprve的关系 Condition中的是nextWaiter

4.最后通过Tail节点从后往前在AQS队列查找是否存在当前节点

    final boolean isOnSyncQueue(Node node) {
        //如果节点的waitStatus = -2 表示存在condition队列中 或者 node的上一个节点为null 表示是头节点也不存在AQS队列中
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        if (node.next != null) // 如果下一个节点不为空 只有AQS队列才存在 next和prve的关系 Condition中的是nextWaiter
            return true;
        //从后往前找当前线程节点是否存在AQS队列中
        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;
        }
    }

最后通过LockSupport.park(this);挂起当前线程

此时在看线程B(ConditionNotify)执行逻辑,因为线程B获得了Lock锁

condition.signal()

线程B唤醒当前线程

        public final void signal() {
            //判断当前线程是否获得了锁
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)//如果Condition队列的头节点不为空 此时的头节点应该是上一步被添加的线程A的Node节点
                doSignal(first);
        }

isHeldExclusively()

当前OwnerThread是否等于当前线程

        protected final boolean isHeldExclusively() {
            // While we must in general read state before owner,
            // we don't need to do so to check if current thread is owner
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

doSignal()

        private void doSignal(Node first) {
            do {
                //通过这种方式剔除掉Condition队列中被唤醒的节点线程A
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

transferForSignal()

    final boolean transferForSignal(Node node) {
        /*
         * 如果当前节点的状态=-2 修改为0 
         * 如果更新失败,只有一种可能就是节点被 CANCELLED 了
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        /*
         * 将当前节点加入到AQS的队列中,并返回当前节点的上一个节点 原Tail节点
         */
        Node p = enq(node);
        int ws = p.waitStatus;
        //如果原Tail节点的waitStatus的状态>0 或者 CAS 当前节点的状态为-1失败
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);//唤醒当前节点中的线程A
        return true;
    }

doSingal大概走了三个步骤

1.compareAndSetWaitStatus修改节点状态为0

2.enq(node)将节点添加到AQS队列

3.移除Condition中的节点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gpCVZQ2t-1576632025120)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20191217193110264.png)]

线程B执行完,Lock.unlock()释放锁

线程A获得锁,继续执行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VWSFDkoW-1576632025120)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20191217193939663.png)]

checkInterruptWhileWaiting()

如果当前线程被中断,则调用transferAfterCancelledWait()判断后续的处理方式应该抛出异常还是重新中断

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

transferAfterCancelledWait()

Thread.interrupt()中断线程,unpark()唤醒 会走第一段逻辑

    final boolean transferAfterCancelledWait(Node node) {
        //如果当前节点是Condition节点 替换ws为0
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            //将node节点加入到AQS队列中
            enq(node);
            return true;
        }
        /*
         * 如果当前node不是AQS队列中的节点,循环直到node不是AQS中的节点
         */
        while (!isOnSyncQueue(node))
            Thread.yield();//让出当前cpu时间片
        return false;
    }

reportInterruptAfterWait()

        private void reportInterruptAfterWait(int interruptMode)
            throws InterruptedException {
            if (interruptMode == THROW_IE)//如果为THROW_IE 抛出异常
                throw new InterruptedException();
            else if (interruptMode == REINTERRUPT)//如果是REINTERRUPT 重新中断
                selfInterrupt();
        }

3.总结

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0IyR749U-1576632025120)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20191218091107468.png)]

橙色的线表示ThreadWait 执行await()方法的线程

普通黑色的线表示ThreadSignal线程

1.ThreadWait 线程lock()方法,获得锁成功,同时ThreadSignal线程获得锁失败,加入到AQS队列

2.condition.await()方法挂起线程,将线程加入到Condition队列,并释放lock锁

3.ThreadSignal线程竞争获得lock锁成功

4.condition.signal()方法唤醒ThreadWait 线程,将Condtion队列中的Node节点添加到AQS队列

5.ThreadWait 线程竞争lock锁,执行后续程序

4.参考

腾讯课堂->咕泡学院->mic老师->并发编程基础

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!