• ArrayBlockingQueue原理分析(一)


    概述

    ArrayBlockingQueue是一个阻塞队列,其实底层就是一个数组,说到底层是数组,ArrayList底层也是数组,那它其实也可以作为队列,但是是非阻塞的,那阻塞和非阻塞的区别是什么?区别在于当队列中没有元素的时候就阻塞等待,直到队列中有数据再消费,而如果队列满了之后(队列有界),生产者就要阻塞。下面就总结一下ArrayBlockingQueue的特性。

    • 是一个有界的队列,初始化队列的时候传入队列大小
    • 采用ReentrantLock + Condition实现线程安全和阻塞
    • 底层采用数组存储
    • 生产者和消费者共用一把锁,所以效率一般

    总结来说就是效率一般,容量有限,那既然这么差还要搞一个这个对象,原因就是这个实现起来简单。ArrayBlockingQueue的插入和删除操作都比较简单,但是里面有一个东西其实还挺复杂的,就是Itrs,迭代器,我打算写两篇博客,本篇介绍插入和删除等一般的方法,下一篇介绍迭代器。

    继承结构图

     这幅图画出了Java中常用队列的继承结构图,可以看出所有的队列都实现了AbstartQueue,这个抽象类实现了Queue接口,提供了Queue接口中方法的默认实现,继承它可以少写一些不必要的代码,但是Queue接口中没有提供大多数的阻塞方法,所以有了BlockingQueue接口,这个接口中提供很多的阻塞的插入删除方法,而最底层的实现者都是阻塞队列,所以都会实现这个接口。

    属性分析

        /** 队列中元素保存的地方 */
        final Object[] items;
    
        /** items index for next take, poll, peek or remove,这个英文注释很详细 */
        int takeIndex;
    
        /** items index for next put, offer, or add */
        int putIndex;
    
        /** Number of elements in the queue */
        int count;
    
        /** Main lock guarding all access */
        final ReentrantLock lock;
    
        /** Condition for waiting takes,处理消费者线程的 */
        private final Condition notEmpty;
    
        /** Condition for waiting puts,处理生产者线程的 */
        private final Condition notFull;
    
        /**
         * Shared state for currently active iterators, or null if there
         * are known not to be any.  Allows queue operations to update
         * iterator state.
    * 迭代器,由于一个队列可以有多个迭代器,所以在该队列中,迭代器通过链表连接起来,itrs属性就是链表头
    * 通过这个头,就可以找到所有的迭代器,下一篇文章会具体分析
    */
    transient Itrs itrs = null;

    构造方法

    public ArrayBlockingQueue(int capacity) {
            this(capacity, false);
        }
    
    public ArrayBlockingQueue(int capacity, boolean fair) {
            if (capacity <= 0)
                throw new IllegalArgumentException();
            //初始化数组,指定容量,不会扩容,固定
            this.items = new Object[capacity];
            //默认是非公平锁,下面会解释一下这个
            lock = new ReentrantLock(fair);
            notEmpty = lock.newCondition();
            notFull =  lock.newCondition();
        }

    关于这里的公平锁,在网上看资料时,发现有的胖友写的文章中说,这里如果是公平锁,如果队列满了,生产者阻塞了非常多,那等待最久的生产者线程会先竞争到锁,别的阻塞线程不能跟他竞争锁,其实这么解释的结论是对的,就是如果是公平锁,那等最久的确实先竞争到锁,但是原因并不是上面说的原因,即便是非公平锁,如果没有新的生产者加入竞争锁,也是等最久先竞争到锁,这里公平锁的作用是说,当多个消费者线程消费多个元素之后,唤醒多个生产者,这时候如果有新的生产者加入,这些生产者需要等待刚刚唤醒的生产者执行完之后才可以竞争锁。

    Queue

    public interface Queue<E> extends Collection<E> {
        //添加元素
        boolean add(E e);
        //添加元素
        boolean offer(E e);
        //删除
        E remove();
        //消费元素,其实也是删除
        E poll();
        //当前元素
        E element();
        //当前消费要消费的元素,不会移除队列,只是获取
        E peek();
    }

    BlockingQueue

    //其实这个接口继承于Queue,只不过多添加了几个方法
    public interface BlockingQueue<E> extends Queue<E> {
        //添加元素
        boolean add(E e);
        //添加元素
        boolean offer(E e);
        //添加元素
        void put(E e) throws InterruptedException;
        //添加元素
        boolean offer(E e, long timeout, TimeUnit unit)
            throws InterruptedException;
        //消费元素
        E take() throws InterruptedException;
        //消费元素
        E poll(long timeout, TimeUnit unit)
            throws InterruptedException;
        
        int remainingCapacity();
        //删除元素
        boolean remove(Object o);
    
        public boolean contains(Object o);
    
        int drainTo(Collection<? super E> c);
    
        public boolean contains(Object o);
    
    }

    搞不懂有些方法在Queue中已经定义了,这里还要重复定义。

    ArrayBlockedQueue

    ArrayBlockedQueue实现了上面的队列,下面就对其实现的方法做一个对比。

    生产者方法

    消费者方法

    add(E e):直接调用offer,和offer方法返回不同,如果插入成功,返回true,否则抛出异常

    offer(E e):向队尾插入数据,如果队列满了,就返回插入失败

     poll():从队头获取元素,如果队列为空,返回失败    

    offer(E e, long timeout, TimeUnit unit):向队尾插入数据,

    如果队列满了,阻塞等待一段时间,超时返回插入失败  

    poll(long timeout, TimeUnit unit):从队头获取元素,如果队列为空,等待一段时间,如果超时返回失败

    put(E e):向队尾插入元素,如果队列满了就一直等待,直到队列中有空闲空间

    take():从队头获取元素,如果队列为空,阻塞等待,直到队列中有元素再消费

     

    remove(Object o):删除队列中的元素,可以是队列中的任意元素

    从上面的对比可知,中间三个方法生产者和消费者是相对的。

    add(E e)

    public boolean add(E e) {
            return super.add(e);
        }
    
    //AbstractQueue的实现,直接调用offer方法,不过和offer不同的是,如果插入队列失败
    //直接抛出异常
    public boolean add(E e) {
            if (offer(e))
                return true;
            else
                throw new IllegalStateException("Queue full");
        }

    offer(E e)

        public boolean offer(E e) {
            checkNotNull(e);
            final ReentrantLock lock = this.lock;
            //先加锁,防止多线程出问题
            lock.lock();
            try {
                //如果数组满了,返回false
                if (count == items.length)
                    return false;
                else {
                    //下面分析这个方法
                    enqueue(e);
                    return true;
                }
            } finally {
                lock.unlock();
            }
        }

    进入#enqueue(E e)

        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();
        }

    这个方法实现很简单,不过生产者的几个方法基本都是调用这个方法。

    offer(E e, long timeout, TimeUnit unit)

    public boolean offer(E e, long timeout, TimeUnit unit)
            throws InterruptedException {
    
            checkNotNull(e);
            //获取传入的超时时间,转为纳秒
            long nanos = unit.toNanos(timeout);
            final ReentrantLock lock = this.lock;
            //获取可中断锁
            lock.lockInterruptibly();
            try {
                //如果队列满了,进入循环
                while (count == items.length) {
                   //下面那个返回负数,这里直接return一个false结束
                    if (nanos <= 0)
                        return false;
                    //阻塞固定的时间,如果超时之后会返回一个0或者负数
                    nanos = notFull.awaitNanos(nanos);
                }
                //还是调用这个方法,就不分析了
                enqueue(e);
                return true;
            } finally {
                lock.unlock();
            }
        }

    这个方法和offer(E e)有两点不同:

    • 使用的锁是可中断锁,就是说如果在等待过程中,线程被中断了会抛出一个异常
    • 使用了超时等待,如果超时才会返回false

    put(E e)

        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();
            }
        }

    ok,到此就把生产者的方法分析完了,其实都是调用的enqueue方法,很简单。

    用最帅的陈永仁做分割线。。。

          

    poll(E e)

        public E poll() {
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                //如果队列为空返回null,否则之后后面的方法
                return (count == 0) ? null : dequeue();
            } finally {
                lock.unlock();
            }
        }

    进入dequeue()方法

    private E dequeue() {
            // assert lock.getHoldCount() == 1;
            // assert items[takeIndex] != null;
            final Object[] items = this.items;
            @SuppressWarnings("unchecked")
           //从队列中获取
            E x = (E) items[takeIndex];
            //删除这个元素
            items[takeIndex] = null;
            //判断是不是到队尾了,如果到队尾了就从头开始
            if (++takeIndex == items.length)
                takeIndex = 0;
            count--;
            //如果该队列存在迭代器,更新里面一些信息,后面会稍微说明一下
            if (itrs != null)
                itrs.elementDequeued();
            //通知生产者线程
            notFull.signal();
            return x;
        }

    上面的过程都非常简单,这里提一下itrs这部分代码,举个例子,假设通过如下代码获取了一个迭代器

    ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue(10);
    Iterator<Integer> iterator = queue.iterator();

    在迭代器初始化时候,其第一个要迭代的元素和消费者要消费的元素是同一个,在上面的代码中如果消费者消费元素,把队列中所有的元素消费完了,但是迭代器还没有运行,这个时候就需要更新迭代器中的一些参数,不让它迭代了,因为队列已经为空了,这里只是提一下,下一篇文章会详细介绍。

    poll(long timeout, TimeUnit unit)

        public E poll(long timeout, TimeUnit unit) throws InterruptedException {
            long nanos = unit.toNanos(timeout);
            final ReentrantLock lock = this.lock;
            lock.lockInterruptibly();
            try {
                while (count == 0) {
                    if (nanos <= 0)
                        return null;
                    nanos = notEmpty.awaitNanos(nanos);
                }
                return dequeue();
            } finally {
                lock.unlock();
            }
        }

    take(E e)

        public E take() throws InterruptedException {
            final ReentrantLock lock = this.lock;
            lock.lockInterruptibly();
            try {
                while (count == 0)
                    notEmpty.await();
                return dequeue();
            } finally {
                lock.unlock();
            }
        }

    remove(Object o)

    public boolean remove(Object o) {
            if (o == null) return false;
            final Object[] items = this.items;
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                if (count > 0) {
                    final int putIndex = this.putIndex;
                    int i = takeIndex;
                    do {
                       //遍历队列,寻找要删除的元素
                        if (o.equals(items[i])) {
                            //执行删除,之后移动数组中的元素,把删除的元素的空位置给挤占掉
                            removeAt(i);
                            return true;
                        }
                        if (++i == items.length)
                            i = 0;
                    } while (i != putIndex);
                }
                return false;
            } finally {
                lock.unlock();
            }
        }

    进入removeAt(i)

    void removeAt(final int removeIndex) {
            // assert lock.getHoldCount() == 1;
            // assert items[removeIndex] != null;
            // assert removeIndex >= 0 && removeIndex < items.length;
            final Object[] items = this.items;
            //如果删除的就是下一个要消费的元素
            if (removeIndex == takeIndex) {
                // removing front item; just advance
                items[takeIndex] = null;
                if (++takeIndex == items.length)
                    takeIndex = 0;
                count--;
                if (itrs != null)
                    itrs.elementDequeued();
            } else {
                // an "interior" remove
    
                // slide over all others up through putIndex.
                final int putIndex = this.putIndex;
                //移动元素
                for (int i = removeIndex;;) {
                    int next = i + 1;
                    if (next == items.length)
                        next = 0;
                    if (next != putIndex) {
                        items[i] = items[next];
                        i = next;
                    } else {
                        items[i] = null;
                        this.putIndex = i;
                        break;
                    }
                }
                count--;
                if (itrs != null)
                    //下一篇文章分析
                    itrs.removedAt(removeIndex);
            }
            //通知生产者线程
            notFull.signal();
        }

    以上就是ArrayBlockingQueue常用方法,还有几个比如contains,toString,toArray都很简单,就不贴代码了。

    总结

    ArrayBlockingQueue总的来说在上面列的结构图中算是最简单的一个,在概述中也说了,其实这里面复杂的是迭代,队列其实很简单,下一篇文章分析迭代过程。

                                  

  • 相关阅读:
    Redis(三)、Redis主从复制
    Redis(二)、Redis持久化RDB和AOF
    Redis(一)、Redis五种数据结构
    docker(部署常见应用):docker部署redis
    docker(三):Harbor 1.8.0 仓库的安装和使用
    Redis List 命令技巧
    数据库——MySQL——事务
    数据库——MySQL
    数据库——MySQL——权限管理
    数据库——MySQL——多表查询
  • 原文地址:https://www.cnblogs.com/gunduzi/p/13659370.html
Copyright © 2020-2023  润新知