并发编程-Condition实现阻塞队列及源码解析

前言

上篇文章我们使用了wait&notify实现阻塞队列实现了一个阻塞队列,今天我们来看看如何用ReentrantLock的Condition对象来实现阻塞队列

实践

public static void main(String[] args) {
        ConditionQueue conditionQueue = new ConditionQueue(5);
        for (int i = 0; i < 10; i++) {
            final int a = i;
            new Thread(() -> {
                try {
                    conditionQueue.put(a);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    conditionQueue.get();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }

    }
    static class ConditionQueue{

        private LinkedList<Integer> queue = new LinkedList<>();

        private final Lock lock = new ReentrantLock();

        //生产者
        private final Condition producer = lock.newCondition();

        //消费者
        private final Condition consumer = lock.newCondition();

        //最大容量
        private int max;

        ConditionQueue(int max){
            this.max = max;
        }

        boolean isFull(){
            return queue.size() == max;
        }

        boolean isEmpty(){
            return queue.size() == 0;
        }

        void put(int i) throws InterruptedException {
            lock.lock();
            while (isFull()){
                producer.await();
            }
            queue.add(i);
            consumer.signalAll();
            System.out.println("插入数据" + i);
            lock.unlock();
        }

        Integer get() throws InterruptedException {
            lock.lock();
            while (isEmpty()){
                consumer.await();
            }
            Integer s = queue.removeFirst();
            producer.signalAll();
            lock.unlock();
            System.out.println("取出数据" + s);
            return s;
        }
    }

那么condition是怎样实现阻塞的呢?
我们上面使用了await方法和signlAll方法
下面我们来看看这两个方法到底做了什么?

源码解析

进入源码我们可以发现lock.newCondition()其实是新建了一个ConditionObject对象,而ConditionObject是AbstractQueuedSynchronizer的一个内部类,ConditionObject本身会维护一个等待队列(这里和之前ReentrantLock的CLH队列要区分开)

await方法

首先我们来看看await方法

 public final void await() throws InterruptedException {
            //判断线程是否被中断
            if (Thread.interrupted())
                throw new InterruptedException();
            //把当前的node节点加入condition的等待队列中
            Node node = addConditionWaiter();
            //释放当前线程占用的锁
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            //判断节点是否在等待队列中,这里的等待队列指的是CLH队列
            //注意这里刚开始是肯定不会在等待队列中的,只有当线程被唤醒后,才可能会在CLH队列中
            while (!isOnSyncQueue(node)) {
                //如果不在队列中,直接挂起自己
                LockSupport.park(this);
                //这里是看线程有没有被interrupted过,这里分为3种情况
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            //然后开始竞争锁
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                //清除掉所有的节点状态不等于condition的节点
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                //这里是看线程有没有被interrupted过,对当前线程做补偿或者抛出异常
                reportInterruptAfterWait(interruptMode);
        }

看下加入等待队列的addConditionWaiter方法,这个方法其实是把一个CONDITION状态的节点放入等待队列中,注意这里的NODE节点和ReentrantLock里的NODE节点是同一个对象

 private Node addConditionWaiter() {
            //队列中的最后一个节点
            //这个node对象和之前CLH队列中的是一样的
            Node t = lastWaiter;
            if (t != null && t.waitStatus != Node.CONDITION) {
                //清除掉所有的节点状态不等于condition的节点
                unlinkCancelledWaiters();
                //重新给t节点赋值
                t = lastWaiter;
            }
            //创建节点
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            //把节点放在队列的最后面
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

然后看下isOnSyncQueue方法,这个方法是判断当前节点是否在CLH队列中,如果在

final boolean isOnSyncQueue(Node node) {
        //如果节点状态为condition或者node是第一个节点
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        //next不等于null说明一定在等待队列中
        if (node.next != null) // If has successor, it must be on queue
            return true;
        /*
         * node.prev can be non-null, but not yet on queue because
         * the CAS to place it on queue can fail. So we have to
         * traverse from tail to make sure it actually made it.  It
         * will always be near the tail in calls to this method, and
         * unless the CAS failed (which is unlikely), it will be
         * there, so we hardly ever traverse much.
         */
        //这里就是一个兜底的,从后往前遍历CLH队列,看能不能找到当前node
        return findNodeFromTail(node);
    }

checkInterruptWhileWaiting方法是判断线程在被唤醒的时候,是否被中断过

private int checkInterruptWhileWaiting(Node node) {
            //注意如果被signal后,waitStatus的状态是为0的

            //1,没有被中断过 直接返回0
            //2,在被signal前就中断  transferAfterCancelledWait返回true
            //3,在signal后被中断的  transferAfterCancelledWait返回false
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        }

然后后面的acquireQueued方法,大家应该很熟悉了,就是之前的ReentranLock的竞争锁的方法,这里就不展开了,有兴趣的同学可以看看上一期reentrantLcok加锁解锁源码

await方法流程总结

线程在执行await方法的时候,会把自己加入到conditionObject的等待队列中,然后释放自身的锁资源,
判断自身是否在CLH队列中,这里有两种情况:
1,不在CLH队列,注意没有被signal的线程是不会在CLH队列中的,这时会直接park自己,等待被unpark
2,在CLH队列,说明此时已经被signal,然后进入下一步,进入竞争lock锁的流程

然后我们来看下signal是怎么把节点放入CLH的

signal方法解析

signal方法

public final void signal() {
            //判断是否是拥有锁的线程在操作
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            //队列的第一个节点
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

然后看下doSignal方法,如果是signalAll方法的话,这里其实就是while循环整个队列,对所有节点调用transferForSignal方法

private void doSignal(Node first) {
            do {
                //设置第一个节点
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
                //如果唤醒不成功的话,或者下一个节点不为空 就去唤醒下一个节点,直到成功唤醒一个节点为止
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

再看下transferForSignal方法

    final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
        //把当前节点的状态设置为0(等待队列中的节点状态本身都为CONDITION,在await方法中设置的)
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        /*
         * Splice onto queue and try to set waitStatus of predecessor to
         * indicate that thread is (probably) waiting. If cancelled or
         * attempt to set waitStatus fails, wake up to resync (in which
         * case the waitStatus can be transiently and harmlessly wrong).
         */
        //把当前节点放入CLH队列中,因为可能还有其他节点也需要等待唤醒
        //注意这里返回的p节点,是当前node的前置节点
        Node p = enq(node);
        int ws = p.waitStatus;
        //如果前置节点被CANCELLED,或者前置节点的状态已经被改变了则唤醒线程
        //其实这里是一个保险的作用,目的就是确保前置节点的状态一定是SIGNAL,这样的话,在unlock的时候就会unpark
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            //唤醒一次线程
            LockSupport.unpark(node.thread);
        return true;
    }
signal方法总结

线程执行signal方法的时候,会先判断自身是否拥有锁,然后唤醒队列的第一个节点,唤醒时会先把节点的等待状态设置为0,然后把当前节点放入CLH队列中,等待获取锁成功

总结

Condition的阻塞队列其实是基于AbstractQueuedSynchronizer的内部类ConditionObject来实现的,
ConditionObject内部维护了一个队列,和Lock的CLH队列是相互独立的

比如上面例子中的producer和consumer,其实就可以理解为两个队列,当调用
producer.await方法时: 往producer队列中放入一个任务节点,并且阻塞自己
producer.singal方法时: 往CLH的队列中放入producer队列的第一个任务节点,然后等待抢锁成功被unpark
可以这么理解,CLH队列中不仅有producer的节点而且有consumer的节点

也就是说
Condition队列是控制线程有没有资格去抢锁
CLH队列是控制线程获取锁的顺序

希望大家不要混淆这两个队列

结尾

有啥理解不当之处希望大家能指出来,共勉

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值