Android之双端队列ArrayDeque源码解析

在之前分析AsyncTask源码的时候有看到其内部自定义线程池里使用到了ArrayDeque这个类去存放线程,今天这篇文章就对它进行解析。

看到ArrayDeque这个名字会发现跟我们频繁使用的ArrayList很像,都是用其内部维护的数组存储数据,都是线程不安全的实现。来看看Google的介绍

Resizable-array implementation of the {@link Deque} interface. Array deques have no capacity restrictions; they grow as necessary to support usage. They are not thread-safe; in the absence of external synchronization, they do not support concurrent access by multiple threads. Null elements are prohibited. This class is likely to be faster than {@link Stack} when used as a stack, and faster than {@link LinkedList} when used as a queue.

大概意思是实现了Deque接口,且大小可变,没有容量的限制,不是线程安全的,在外面没有同步的情况下,不支持多线程访问。不支持添加null元素,当作为栈使用的时候比Stack更快,当作为队列使用的时候比LinkedList更高效。

这里面的LinkedList应该都知道,而Stack这个类大家可能会见的少,Stack是继承Vector这个类的,而Vector跟ArrayList是一个父亲下来的;Vector是google设计的线程安全的数组,而ArrayList是线程不安全,但是高效的数组;突然想起来以前遇到一个面试问题,讲述一下为什么有了StringBuild,还要设计一个StringBuffer,其实跟Vector和ArrayList的存在一样,为了适应不同业务需求而分别存在。你单线程高效但是并发不安全,你并发安全但是单线程不高效。所以Stack也是线程安全的(是一种后进先出的数据结构,只能在一端进行压栈或者出栈的数据操作)。

接下来看看源码里定义的变量

/**
     * The array in which the elements of the deque are stored.
     * The capacity of the deque is the length of this array, which is
     * always a power of two. The array is never allowed to become
     * full, except transiently within an addX method where it is
     * resized (see doubleCapacity) immediately upon becoming full,
     * thus avoiding head and tail wrapping around to equal each
     * other.  We also guarantee that all array cells not holding
     * deque elements are always null.
     * transient修饰符是避免在实现Serializable接口后被自动序列化进行传递
     * 存储元素的数组,它的长度是2的n次方,
     */
    transient Object[] elements; // non-private to simplify nested class access

    /**
     * The index of the element at the head of the deque (which is the
     * element that would be removed by remove() or pop()); or an
     * arbitrary number equal to tail if the deque is empty.
     * 头部元素索引,并不是指数组第0个元素
     */
    transient int head;

    /**
     * The index at which the next element would be added to the tail
     * of the deque (via addLast(E), add(E), or push(E)).
     * 下一次添加到队列尾部的元素索引,即当前最后一个元素的索引+1
     */
    transient int tail;

    /**
     * The minimum capacity that we'll use for a newly created deque.
     * Must be a power of 2.
     * 数组最小长度
     */
    private static final int MIN_INITIAL_CAPACITY = 8;

主要维护了一个数组,长度不限,长度为2的n次方,且记录了队首和队尾的位置,保证能进行双端操作

再看看构造方法

/**
     * Constructs an empty array deque with an initial capacity
     * sufficient to hold 16 elements.
     * 初始化一个长度为16的数组,这个长度是可变的
     */
    public ArrayDeque() {
        elements = new Object[16];
    }

    /**
     * Constructs an empty array deque with an initial capacity
     * sufficient to hold the specified number of elements.
     *
     * @param numElements  lower bound on initial capacity of the deque
     *
     *                     初始化一个指定容量的数组
     */
    public ArrayDeque(int numElements) {
        //分配数组空间
        allocateElements(numElements);
    }

    /**
     * Constructs a deque containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.  (The first element returned by the collection's
     * iterator becomes the first element, or <i>front</i> of the
     * deque.)
     *
     * @param c the collection whose elements are to be placed into the deque
     * @throws NullPointerException if the specified collection is null
     *
     * 将指定集合存入数组
     */
    public ArrayDeque(Collection<? extends E> c) {
        //分配数组空间
        allocateElements(c.size());
        //将元素添加到数组
        addAll(c);
    }
/**
     * Allocates empty array to hold the given number of elements.
     *
     * @param numElements  the number of elements to hold
     *
     * 分配一个空数组存储给定数量元素
     * 数组大小肯定是大于或者等于numElements值的,因为要求必须是2的n次方
     * 假如传入的是11,那2的3次方=8<11;2的4次方=16>11;所以最终数组大小是16
     */
    private void allocateElements(int numElements) {
        int initialCapacity = MIN_INITIAL_CAPACITY;
        // Find the best power of two to hold elements.
        // Tests "<=" because arrays aren't kept full.
        if (numElements >= initialCapacity) {
            initialCapacity = numElements;
            initialCapacity |= (initialCapacity >>>  1);
            initialCapacity |= (initialCapacity >>>  2);
            initialCapacity |= (initialCapacity >>>  4);
            initialCapacity |= (initialCapacity >>>  8);
            initialCapacity |= (initialCapacity >>> 16);
            initialCapacity++;

            if (initialCapacity < 0)    // Too many elements, must back off
                initialCapacity >>>= 1; // Good luck allocating 2^30 elements
        }
        elements = new Object[initialCapacity];
    }

构造方法主要就是初始化一个给定大小的数组,给数组分配空间的方法是allocateElements(int numElements),我们看下这里面的逻辑:

  1. 首先判断传入的值是否小于最小值,如果小于,就按最小值初始化这个数组
  2. 如果>=最小值,就通过运算计算数组大小,计算方式是以传入值做位运算得到数组大小
  3. 看位运算逻辑:

>>>

: 是无符号右移运算符,把二进制数往右移动指定数量位置,左边空出来的位用0补充,右边移出去的位舍弃,即高位补零,低位舍弃;
例如 5 >>> 1,5的二进制是101,往右移一位10,高位补零就是010,即5 >>> 1 = 2;
例如-5 >>> 1,同样的道理,在计算机中负数是以其正数对应的补码表示;-5的二进制数计算方法是先获取5的原码(一个整数按照绝对值得到的二进制数),再得到反码(将二进制数按位取反得到的数),再将反码加1得到补码;
5的原码全称是00000000 00000000 00000000 00000101,int类型数是32位
5的反码就是 11111111 11111111 11111111 11111010
5的补码就是 11111111 11111111 11111111 11111011
这样-5的二进制就是11111111 11111111 11111111 11111011,将其右移一位得到
01111111 11111111 11111111 11111101 = 2147483645
这样-5 >>> 1 = 2147483645,

|=

:也是位运算符一种,例如a |= b,等价于a = a|b,即把a和b按 位或 操作后的值赋值给a;位或 操作逻辑是第一个操作数的第n位与第二个操作数的第n位 只要有一个是1,那么结果的第n为也为1,否则为0;例如 5|3 ,5的二进制是101,3的二进制是011,对应位进行比较,得出111,即 5|3 = 7。
了解了计算方法,再回到allocateElements方法中:
假设我们传入的值是11,看第一句代码:initialCapacity |= (initialCapacity >>> 1);这个等价于initialCapacity = initialCapacity | (initialCapacity >>> 1);initialCapacity 值等于11,先计算11无符号右移一位,11的二进制是1011,右移后是0101,即5;接着计算11 | 5,11的二进制是1011,5的二进制是0101,位或操作后得到值是1111=15,这样initialCapacity = 15;再看第二句initialCapacity |= (initialCapacity >>> 2); 相当于initialCapacity = 15 | (15 >>>2),这样算出来的值依然是15;后面三个位运算就不一一演示了,这样位运算结束后得到的值是15,再通过initialCapacity++,得到数组最终长度是16,正好是2的4次方。

继续看添加元素方法

/**
     * Inserts the specified element at the front of this deque.
     *
     * @param e the element to add
     * @throws NullPointerException if the specified element is null
     * 将元素从数组的最后一个位置向前依次存放
     */
    public void addFirst(E e) {
        if (e == null)
            throw new NullPointerException();
        //插入元素的同时计算head的值
        //进行与操作是为了防止数组越界,比如刚开始head=0,head-1=-1,这样肯定会出现问题的
        elements[head = (head - 1) & (elements.length - 1)] = e;
        //判断数组是否满了
        if (head == tail)
            doubleCapacity();
    }

    /**
     * Inserts the specified element at the end of this deque.
     *
     * <p>This method is equivalent to {@link #add}.
     *
     * @param e the element to add
     * @throws NullPointerException if the specified element is null
     * 将元素从数组的第一个位置依次向后存放
     */
    public void addLast(E e) {
        if (e == null)
            throw new NullPointerException();
        elements[tail] = e;
        //判断数组是否满了
        if ( (tail = (tail + 1) & (elements.length - 1)) == head)
            doubleCapacity();
    }

    /**
     * Inserts the specified element at the front of this deque.
     *
     * @param e the element to add
     * @return {@code true} (as specified by {@link Deque#offerFirst})
     * @throws NullPointerException if the specified element is null
     */
    public boolean offerFirst(E e) {
        addFirst(e);
        return true;
    }

    /**
     * Inserts the specified element at the end of this deque.
     *
     * @param e the element to add
     * @return {@code true} (as specified by {@link Deque#offerLast})
     * @throws NullPointerException if the specified element is null
     */
    public boolean offerLast(E e) {
        addLast(e);
        return true;
    }
     public boolean offer(E e) {
        return offerLast(e);
    }
    public boolean add(E e) {
        addLast(e);
        return true;
    }
/**
     * Doubles the capacity of this deque.  Call only when full, i.e.,
     * when head and tail have wrapped around to become equal.
     * 数组满的时候将数组容量翻倍
     * 这时候head和tail指向同一个元素
     */
    private void doubleCapacity() {
        //assert关键字是断言的意思,指代码执行到这个地方的时候是预期的结果,不会影响到程序的执行
        //可以和if语句有差不多的效果,jdk1.4以后引入
        assert head == tail;
        int p = head;
        int n = elements.length;
        //计算从哪个位置开始复制
        int r = n - p; // number of elements to the right of p
        //将现有数组长度左移一位得到新数组长度
        int newCapacity = n << 1;
        if (newCapacity < 0)
            throw new IllegalStateException("Sorry, deque too big");
        //根据新的长度实例化一个新的数组
        Object[] a = new Object[newCapacity];
        //从elements[p]开始取r个长度的值复制到a[0]~a[r-1]
        System.arraycopy(elements, p, a, 0, r);
        //从elements[0]开始取p个长度的值复制到a[r]~a[r+p-1]
        System.arraycopy(elements, 0, a, r, p);
        //重置数组
        elements = a;
        //重置head和tail值
        head = 0;
        tail = n;
    }

这几个方法一个是从数组头部添加元素,一个从数组尾部添加元素,这就是双端队列;如果数组长度达到限定长度,进行长度翻倍

继续看取数据方法

/**
     * 检索并删除索引为head的元素
     * @throws NoSuchElementException {@inheritDoc}
     */
    public E removeFirst() {
        E x = pollFirst();
        if (x == null)
            throw new NoSuchElementException();
        return x;
    }

    /**
     * 检索并删除索引为tail的元素
     * @throws NoSuchElementException {@inheritDoc}
     */
    public E removeLast() {
        E x = pollLast();
        if (x == null)
            throw new NoSuchElementException();
        return x;
    }

    /**
     * 获取head索引处的值,并把数组该处值置为null,同时通过size()获取的值也会减小1
     * @return
     */
    public E pollFirst() {
        final Object[] elements = this.elements;
        final int h = head;
        //保存head索引处的值
        @SuppressWarnings("unchecked")
        E result = (E) elements[h];
        // Element is null if deque empty
        if (result != null) {
            //删除head索引的值
            elements[h] = null; // Must null out slot
            //计算head+1值作为新的索引,这样与操作是避免h + 1=elements.length 出现数组越界
            head = (h + 1) & (elements.length - 1);
        }
        return result;
    }

    /**
     * 获取tail索引处的值,并把数组该处值置为null,同时通过size()获取的值也会减小1
     * @return
     */
    public E pollLast() {
        final Object[] elements = this.elements;
        //获取tail - 1索引处的值 这样的操作是处理临界情况(当tail为0时)
        final int t = (tail - 1) & (elements.length - 1);
        //保存tail索引处的值
        @SuppressWarnings("unchecked")
        E result = (E) elements[t];
        if (result != null) {
            //删除tail索引处的值
            elements[t] = null;
            //tail指向的是下个要添加元素的索引
            tail = t;
        }
        return result;
    }

    /**
     * @throws NoSuchElementException {@inheritDoc}
     * 获取head索引处的值
     */
    public E getFirst() {
        @SuppressWarnings("unchecked")
        E result = (E) elements[head];
        if (result == null)
            throw new NoSuchElementException();
        return result;
    }

    /**
     * @throws NoSuchElementException {@inheritDoc}
     * 获取tail-1处索引处的值
     */
    public E getLast() {
        @SuppressWarnings("unchecked")
        E result = (E) elements[(tail - 1) & (elements.length - 1)];
        if (result == null)
            throw new NoSuchElementException();
        return result;
    }

    @SuppressWarnings("unchecked")
    public E peekFirst() {
        // 获取head索引处的值,可能为null
        return (E) elements[head];
    }

    @SuppressWarnings("unchecked")
    public E peekLast() {
        //获取tail-1处索引处的值,可能为null
        return (E) elements[(tail - 1) & (elements.length - 1)];
    }

接下来看看简单使用

ArrayDeque<Integer> deque = new ArrayDeque<>();
        deque.addFirst(1);
        deque.addFirst(2);
        deque.addFirst(3);
        deque.addFirst(4);
        deque.addFirst(5);
        deque.addFirst(6);
        deque.addFirst(7);
        deque.addFirst(8);
        for (Integer value:deque) {
            Log.e(TAG,"deque value="+value);
        }

看日志

06-25 19:13:27.463 11323-11323 E/MainActivity: deque value=8
06-25 19:13:27.463 11323-11323 E/MainActivity: deque value=7
06-25 19:13:27.463 11323-11323 E/MainActivity: deque value=6
06-25 19:13:27.463 11323-11323 E/MainActivity: deque value=5
06-25 19:13:27.463 11323-11323 E/MainActivity: deque value=4
06-25 19:13:27.463 11323-11323 E/MainActivity: deque value=3
06-25 19:13:27.463 11323-11323 E/MainActivity: deque value=2
06-25 19:13:27.463 11323-11323 E/MainActivity: deque value=1

这个输入顺序好像跟ArrayList相反的对吧,这是因为addFirst方法是将元素从数组的最后一个位置向前依次存放,可以说是一个后进先出的规则

如果使用addLast方法添加元素,日志如下

06-25 19:22:43.341 19507-19507 E/MainActivity: deque value=1
06-25 19:22:43.341 19507-19507 E/MainActivity: deque value=2
06-25 19:22:43.341 19507-19507 E/MainActivity: deque value=3
06-25 19:22:43.341 19507-19507 E/MainActivity: deque value=4
06-25 19:22:43.341 19507-19507 E/MainActivity: deque value=5
06-25 19:22:43.341 19507-19507 E/MainActivity: deque value=6
06-25 19:22:43.341 19507-19507 E/MainActivity: deque value=7
06-25 19:22:43.341 19507-19507 E/MainActivity: deque value=8

它是一个先进先出的规则,将元素从数组的第一个位置依次向后存放

其它方法也很简单,就不一一演示了。我们回到AsyncTask源码中对ArrayDeque的使用:

mTasks.offer(new Runnable() {
                public void run() {
                    try {
                        r.run();
                    } finally {
                        //r执行结束后执行下一个任务
                        scheduleNext();
                    }
                }
            });
if ((mActive = mTasks.poll()) != null) {
                THREAD_POOL_EXECUTOR.execute(mActive);
 }

通过offer方法添加工作线程,这个方法会调用offerLast(E e),然后调用到addLast(E e),看上面的注释,这个方法将元素从数组的第一个位置依次向后存放,并且改变的是tail的值,head的值一直是0;然后通过poll()方法取线程,会调用pollFirst()方法,这个方法通过head取值,取完值将head+1;这样AsyncTask的工作逻辑就是先添加的线程先执行。

猜你喜欢

转载自blog.csdn.net/qq_30993595/article/details/80799444