前言
最近有时间就看了一下Retrofit库的源码,从而学习一下这个优秀框架的编码,浏览这个框架的过程中发现Dispatcher线程池的调度器使用了ArrayDeque来存储要执行网络请求的线程AsyncCall对象,因此对这个队列底层原理产生极大兴趣,所以进行了解了一般,本文会以真实代码和图片代码的形式描述,以增加可读性。
1、Dispatcher类型中的成员变量:
2、小结:ArrayDeque底层使用数组实现。ArrayDeque中的数组没有容量限制;它根据需要增长以支持使用。它不是线程安全的;在没有外部同步的情况下,它不支持多线程的并发访问。禁止空元素。当用作堆栈时,这个类可能比Stack快,当用作队列时可能比LinkedList快。
ArrayDeque底层使用到的核心运算符&、|、<<、>>>
数据结构往往收不了用到一些运算符,所以想要熟悉核心算法,就需要了解这些运算符的计算。
-
|(或运算符)
运算规则:两个二进制同一位上至少有一个1,则结果中该位为1,否则为0;
比如:20 | 2 = 22。
解析:00010100(十进制为20) & 00000010(十进制为2)-> 00010110(十进制为22)。 -
&(与运算符)
运算规则:两个二进制同一位上都是1,则结果中该位为1,否则为0;
比如:20 & 2 = 0。
解析:00010100(十进制为20) & 00000010(十进制为2)-> 0(十进制为0)。 -
<<(左移运算符)
运算规则:向左移动x位,无论正负数低位(最右边)都补 x 个 0
比如:20 << 2 = 80
解析:00010100(十进制为 20) -> 01010000(十进制为 80) -
(无符号右移运算符)
运算规则:无符号右移 x 位,所谓无符号是与 >> x对比,该操作在移动后,无论正负数高位(最左边)都补0。
比如:20 >>> 2 = 5
解析:00010100(十进制为 20)-> 00000101(十进制为 5)
比如:-20 >>> 2 = 4611686018427387899
解析:1111111111111111111111111111111111111111111111111111111111101100(十进制为 -20) ->0011111111111111111111111111111111111111111111111111111111111011(十进制为 4611686018427387899)
ArrayDeque的创建
ArrayDeque提供三个构造方法创建对象,如下图
把三个方法复制出来可以发现:
//创建一个内部数组长度为16的对象
public ArrayDeque() {
elements = new Object[16];
}
//创建一个内部数组长度为自定义长度的对象
public ArrayDeque(int numElements) {
//重新计算分配制定的数组长度
allocateElements(numElements);
}
//创建一个添加外部集合数据到新队列的对象
public ArrayDeque(Collection<? extends E> c) {
//重新计算分配制定的数组长度
allocateElements(c.size());
//添加外部集合数据到队列当中
addAll(c);
}
创建对象的核心方法,计算双端队列数组的长度,就是如果是自定义一个队列的长度,要经过这个方法一系列运算得到2的幂次方数组长度,此方法的计算原理暂不做解释,可以自己梳理理解:
ArrayDeque核心方法
1、队列头部添加数据
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
/** 获取数组中数据的下标运算,数组的长度就起到至关重要作用,计算头部的下标:head = (head - 1) & (elements.length - 1),该运算简明一点就是:head = (head - 1) */
elements[head = (head - 1) & (elements.length - 1)] = e;
//计算尾部的tail是否等于头部head,等于说明队列中的数组数据已满,需要扩容
if (head == tail)
//开始执行扩容方法
doubleCapacity();
}
2、队列尾部添加数据
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
//直接数据加入尾部,因为tail值下标的存储空间是空的,待数据直接加入
elements[tail] = e;
//计算尾部的tail是否等于头部head,等于说明队列中的数组数据已满,需要扩容
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
//开始执行扩容方法
doubleCapacity();
}
3、队列添加数据过程中,如果判断头部head和尾部tail相等,说明数组已满需要扩容,doubleCapacity方法方法主要负责扩容的逻辑。
private void doubleCapacity() {
//是否成立,否则抛异常
assert head == tail;
//头部下标
int p = head;
//数组长度
int n = elements.length;
//计算出r:头部head下标右边的元素个数
int r = n - p;
//扩容数量,n左移1位等于n乘以2的1次方,如:16X2=32
int newCapacity = n << 1;
if (newCapacity < 0)
throw new IllegalStateException("Sorry, deque too big");
Object[] a = new Object[newCapacity];
//把头部部分的数据复制到新数组的开始位置为0,然后从原数组p下标开始复制,头部往后复制r个数据到新的数组中
//例如原数组是[a,b,c,....,1,2,3],复制后的新数组是[1,2,3,....]
System.arraycopy(elements, p, a, 0, r);
//把结尾部部分的数据复制到新数组的开始位置为r,然后从原数组0下标开始复制,结尾部往后复制p个数据到新的数组中
//例如原数组是[a,b,c,....,1,2,3],由于经过上一轮复制得到的[1,2,3,....],然后再次执行复制后的新数组是[1,2,3,a,b,c....]
System.arraycopy(elements, 0, a, r, p);
// Android-added: Clear old array instance that's about to become eligible for GC.
// This ensures that array elements can be eligible for garbage collection even
// before the array itself is recognized as being eligible; the latter might
// take a while in some GC implementations, if the array instance is longer lived
// (its liveness rarely checked) than some of its contents.
//清除旧数据,并回收内存
Arrays.fill(elements, null);
//新数组赋值
elements = a;
//头部下标设置为0,循环操作
head = 0;
//尾部为旧数组长度值
tail = n;
}
4、获取头部数据,添加头部数据是从右到左顺序添加的,所以下标的数组的下标是递减的,获取头部数据直接通过head下标获取,获取后,删除该下标的内存,并且head下标加1。
public E pollFirst() {
final Object[] elements = this.elements;
final int h = head;
@SuppressWarnings("unchecked")
E result = (E) elements[h];
// Element is null if deque empty
if (result != null) {
elements[h] = null; // Must null out slot
head = (h + 1) & (elements.length - 1);
}
return result;
}
5、获取尾部数据,tail下标指向的内存空间是空的,是待添加数据的下标,所以需要tail自减1后得到的下标,该下标数据就是尾部最后一个数据了,获取到最后一个数据后,清除该下标指向的内存空间。
public E pollLast() {
final Object[] elements = this.elements;
final int t = (tail - 1) & (elements.length - 1);
@SuppressWarnings("unchecked")
E result = (E) elements[t];
if (result != null) {
elements[t] = null;
tail = t;
}
return result;
}
结尾:以上只是简要讲解ArrayDeque核心代码块,该队列类的其他方法大同小异,有兴趣的小伙伴可以去看看源码。