Java并发编程系列 | AQS之条件队列的原理

Java并发编程系列文章

欢迎大家观看我的博客,会不断的修正和更新文章,也欢迎大家一起交流


AQS是锁和一些同步器实现的基础组件,它提供了一个阻塞队列,并且实现了入队列和出队列和信号通知下一个节点,以及其他队列操作
相关的方法,同时还有state,tryAcquire是由子类去自己定义和实现的
  • 在ReentrantLock 中,state表示当前线程获取锁的重入次数,在ReentrantReadWriteLock中,state高16
    位表示读状态,也就是获取该读锁的次数,低16位表示获取到写锁的线程的重入次数,
    在semaphore中,state表示当前可用信号的数量,在CountDownLatch中,state表示计数器当前的值,然后他们根据自己
    不同的特点来实现tryAcquire方法
  • 在AQS中的阻塞队列中,Node节点的waitStatus总共有五种状态
    1.CANCELLED
    线程被中断,或者超时了,就会在cancelAcquire里将waitStatus置为CANCELLED
    2.SIGNAL
    表示现在Node正在AQS队列等待,需要unpark来唤醒
    3.CONDITION
    表示现在Node正在条件队列里等待
    4.PROPAGATE
    表示释放共享资源时需要通知其他节点
    5.0
    初始值,不代表以上4个任何一种状态
  • 条件队列的实现

    当使用reentrantLock时,在lock和unlock中使用condition.await()和condition.signal()时,其实就像是在synchronized中使用object.wait()和object.nofity()一样,都是起到一个阻塞和通知的作用,不过使用reentrantLock所不同的是,可以使用多个条件变量,来在lock和unlock之间进行阻塞和通知的,而synchronized只能在代码块中使用被synchronized修饰的那个变量来进行wait和notify。

    • await的流程
      当使用await()方法时,先生成一个当前节点添加到到条件队列,再把state置为0,取消线程独占并释放锁,接下来park等待signal执行且把节点重新放回AQS队列中,然后再尝试在队列里等待unlock通知的这样的一个流程。
            public final void await() throws InterruptedException {
                if (Thread.interrupted())
                    throw new InterruptedException();
                Node node = addConditionWaiter();//生成一个当前节点添加到到条件队列
                int savedState = fullyRelease(node);//把state置为0,取消线程独占并释放锁
                int interruptMode = 0;
                while (!isOnSyncQueue(node)) {
                    //如果这个node还没有出现在AQS阻塞队列里面,意思就是还在条件队列中
                    //就会park阻塞当前线程
                    LockSupport.park(this);
                    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                        break;
                }
                //到这来的时候,就表示这个条件已经被signal并且又被放入阻塞队列,且又被通知了
                //这里就会恢复state的状态为原来savedState这么多
                //并等待被前面的节点unpark通知或者打断
                if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                    interruptMode = REINTERRUPT;
                if (node.nextWaiter != null) // clean up if cancelled
                    //如果条件队列中有节点已经超时或者被取消了,在这里就会被清理掉
                    unlinkCancelledWaiters();
                if (interruptMode != 0)
                    //如果是被打断的,就会在这里抛出InterruptedException
                    reportInterruptAfterWait(interruptMode);
            }
    
    • signal的流程
      注意,signal的流程,signal除了被取消和通知的会unpark它的线程外,就不会unpark执行通知了,所以在这里,signal的作用其实就是将节点从条件队列移出和往AQS队列中加入节点。
      所以我刚开始还有一点疑问的就是,这个signal一般情况下不执行unpark来通知,那await代码里的LockSupport.park(this),到底是什么时候被谁通知的?
      后来调了调代码,发现原来是在signal之后,执行unlock时通知的,因为执行unlock后又会自动的去AQS队列里面找节点来通知,而signal就把await放到条件队列里的节点再放回AQS队列,这样通知的时候就也是通知await的线程了。
            public final void signal() {
                //如果该锁不是被这个线程独占的,就抛出异常
                //这就决定了signal必须要在reentrantLock的lock和unlock之间使用
                if (!isHeldExclusively())
                    throw new IllegalMonitorStateException();
                Node first = firstWaiter;
                if (first != null)
                    //将条件队列中的第一个节点出队列,并将其放入AQS队列
                    doSignal(first);
            }
    
            private void doSignal(Node first) {
                do {//将firstWaiter 指针指向下一个节点,
                    //并让原来firstWaiter的下一个节点置为null,相当于把该节点移除了
                    if ( (firstWaiter = first.nextWaiter) == null)
                        lastWaiter = null;
                    first.nextWaiter = null;
                } while (!transferForSignal(first) &&
                         (first = firstWaiter) != null);
            }
    
        final boolean transferForSignal(Node node) {
            //使用原子操作将waitStatus设置为0,如果没设置成功,那就是被取消了,直接返回false
            if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
                return false;
            //将节点插入AQS队列
            Node p = enq(node);
            int ws = p.waitStatus;
            if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
                //如果说这个节点被取消了,就直接通知
                //节点没有被取消的话,还会将waitStatus状态设置为SIGNAL
                LockSupport.unpark(node.thread);
            return true;
        }
    

    一个简单的流程图

    在这里插入图片描述

    一个小问题

    为什么像object.wait()和notify(),以及condition.await()和signal(),他们为什么必须要放在自己的同步代码块中,否则就会报错呢?是出于什么目的这样设计的呢?

    • 其实就是这一个目的:这是为了保证其并发安全性
    • 就以ArrayBlockingQueue来举例
        public void put(E e) throws InterruptedException {
            checkNotNull(e);
            final ReentrantLock lock = this.lock;
            lock.lockInterruptibly();
            try {
                while (count == items.length)
                    notFull.await();
                enqueue(e);
            } finally {
                lock.unlock();
            }
        }
        /**
         * Inserts element at current put position, advances, and signals.
         * Call only when holding lock.
         */
        private void enqueue(E x) {
            // assert lock.getHoldCount() == 1;
            // assert items[putIndex] == null;
            final Object[] items = this.items;
            items[putIndex] = x;
            if (++putIndex == items.length)
                putIndex = 0;
            count++;//设置条件
            notEmpty.signal();
        }
        public E take() throws InterruptedException {
            final ReentrantLock lock = this.lock;
            lock.lockInterruptibly();
            try {
                while (count == 0)//判断条件
                    notEmpty.await();
                return dequeue();
            } finally {
                lock.unlock();
            }
        }
    
    • 我们就依据notEmpty来分析,首先可以直接在代码上看到,notEmpty是一直处于lock和unlock代码块中的,并且enqueue上都有注释提示了我们。
    • 一般来说,使用condition的话,都会有带着一定的条件变量使用的,并且条件的判断和赋值都是在同步块中的,这种设计在java并发包中随处可见,而在这里,notEmpty就使用了count来进行判断。
    • 如果说,await()和signal()可以不用放在同步块中,那么,就会产生并发的问题
    时间节点1时间节点2时间节点3时间节点4最终结果
    线程A在take中判断count为0执行notEmpty.await()会一直阻塞
    线程B在enqueue中count++执行notEmpty.signal()通知
    时间节点1时间节点2时间节点3时间节点4最终结果
    线程A在take中判断count为1不执行notEmpty.await()取数据
    线程B在take中判断count为1不执行notEmpty.await()取数据
    线程C在enqueue中count++执行notEmpty.signal()通知
    • 上面这两种情况都是严重的并发错误,必须是要将他们同步起来的,所以说,object.wait()和notify(),以及condition.await()和signal(),他们必须要放在自己的同步代码块中。

    总结

    条件队列也是非常重要的一个知识点,它类似于object.wait()但是却可以支持多个条件,在java的并发包里也有大量的使用到,比如一些信号量的底层实现,还有所有的BlockingQueue这些,它有notEmpty和notFull这两个条件,用来控制存和取时的阻塞,像这样的要支持多个条件的,object.wait()它就做不到。但是如果想要了解BlockingQueue的原理,但你不了解条件队列,那么阅读它的源码时也会有各种困难。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值