虽然在某程序员最喜欢的工具调查中,90%的人选择了eclipse,但是我这里还是要强烈推荐idea.我在来我目前公司之前也是用eclipse,但是公司里的程序员用的都是idea,然后自己也尝试转型,一用上以后发现我已经深深爱上它了,真的是太好用了。尤其是读源码和源码debug方面,真的是非常便捷,在这里强烈推荐。有人说这个要收费,我想说呵呵,我自己也是百度的2016破解,可惜没有做记录,目前网上还是有注册服务器的,你需要多一点耐心就好。
好了废话不多说,开搞。今天读的源码的是ArrayBlockingQueue。首先来看一张类接口关系图。
其实这个类主要的方法是在BlockingQuere接口中定义好的。所有的实现除了add方法调用了abstarctQueue之外,其他都是自己实现的。下面我们来看看他是怎么实现的。
第一个final的object数组即是实现的容器,这里我们要强调一下,如果你了解类的加载过程,那你就对这个一点也不奇怪了,如果不懂你可以去看一下类的加载过程,这里的final没有被static修饰,也就代表着它的初始化是在new对象的Init方法里初始化的。init也就是我们的构造函数执行的过程。
takeindex代表着下一个可取的数组下标。初始化为0,然后调用poll,remove,take方法都会进行++操作,如果>=数组容量长度,则重置为0
putindex代表着下一个可存放的数组下标,初始化为0,如果调用add,offer,put则进行++操作,如果>=数组容量长度,则重置为0
count代表总共存放了多少个对象,
下面维护了一把锁,2个condition,notempty用于维护的是空阻塞,这里比如调用poll和take的时候如果count为0则会进行阻塞,只要添加一个,则会执行notempty.signl操作唤醒阻塞,具体的情况下会讲解。notFull维护的是满阻塞,调用offer和put如果数组满了则会阻塞。
上面这些就组成了我们实现这个队列的所有必要的元素。
当然这里重点讲解的是重点几个API实现的过程。下面我们一个一个的展开讲。
首先我们来看下构造函数,
其中第一个需要指定初始化大小,这里的数组大小一旦指定是不可变的。
默认初始化的锁为非公平锁,如果不懂公平锁非公平锁,请自行查看
如果需要指定公平锁,则使用2个参数的构造函数,下面看三个参数的
初始化动作也是调用了2个参数的,但是多了一步,我们可以把别的集合直接转到这个队列中去。
============================下面看方法实现================================
首先看add、put、offer方法。首先说下三个方法的区别:
add,添加失败会抛出异常
put,如果数组满则会阻塞到有空位为止
offer,添加失败会返回false。
下面先来看add方法。
add方法直接调用了abstractQueue中的add方法,
从这里看为什么我们add方法会抛出异常,原来就是这么来的,如果添加失败则抛出异常。添加动作能交给offer去做,添加失败返回false,然后就抛出异常,其实就是对offer的返回形式进行了一次改变而已嘛!
下面我们看offer操作
offer提供了2个操作,一个是只有一个待添加元素的参数,另外一个是多了2个参数,一个是long类型基本数据类型,一个是jdk1.8的新的时间对象工具。不要着急,我们一个个来看。
先来看1个参数的:
一进来首先对它进行null值检测,其实实现很简单,就是如果为null抛出空指针异常。
然后锁定,判断,最终添加操作是在enqueue这个操作里实现的。其实三种添加操作都是通过enqueue方法实现的,所有的取出操作都是dequeue方法实现的。然后如果成功返回true,如果失败返回false.就是这么实现的。
这里的实现很简单,直接放入下一个可存放的位置,然后对下一个可存放下标++操作,然后与数组容量判断,如果满了则置为0,然后对记录总存量的count进行++操作。然后调用非空锁唤醒操作。这里留一个疑问,比如我当前添加满了,然后下一个可存放的位置重置为0,这时候另外一个操作按照下标取走了中间一个位置的对象,或者是末尾位置的对象,这时候数组是有空位置的,这时候我们再存入一个时候会发生什么事情呢?
下面我们看待时间参数的offer方法
第一个参数是待存入元素,第二个参数是超时时间,第三个参数是时间单位。
通过上面的while循环可知,如果阻塞超时,则直接返回false,如果阻塞期间,count变化了,则进入下面的enqueue方法存入对象。实现就这么简单。
还剩最后一个我们看put方法,
put方法就是在while循环里阻塞,然后等待唤醒。这里的lock锁是通过lockInterruptibly();进行获取的,这里我写这的时候也不明白为什么这么写。后面会研究下关于锁的获取的不同方式。
这里我们已经把添加元素的方法都看完了,是不是很简单啊。
下面我们看取的几个实现,包括take,poll,peek。
take方法是阻塞获取,如果为空,则阻塞,直到有对象了再获取,从数组中删除该对象
poll方法是如果为空返回null,从数组中删除该对象
peek方法是直接返回该对象,并不会从数组中删除
先来看take的实现方式
很简单,如果为空则阻塞,一旦有元素则唤醒,然后调用dequeue方法获取,我们看看dequeue的实现
这里先把下一个可获取元素位置的下标取出来,然后把这个位置修改为null,然后在进行对可获取下标位置的修改,如果==数组容量则重置为0.然后对非满进行唤醒操作。
poll方法有2个实现,一个是如果为空立即返回null,另外一个就是阻塞等待时间,如果超时立即返回。
用了一个三目运算符,soeasy
这里也会在while循环里进行阻塞,如果超时返回null,如果有新的对象进来,则调用dequeue。
下面看peek方法
peek直接返回了下一个可取位置的对象,不进行删除操作。
到此为止,我们的添加和获取都看过了,下面来看remove方法,有4个方法
remove(o),删除指定对象
remove()删除一个
removeAll删除集合中的全部对象
removeIf()
先来看不带参数的,remove方法直接返回了当前移除的对象,删除的呢正好是下一个可获取的对象。
移除操作实际调用的是abstractQueue中的remove,最后实际调用的还是poll方法。
下面看删除指定对象
这里首先我们找出需要删除对象的下标,通过指定下标取删除
下面看看删除指定位置的,
如果指定删除的下标正好等于可获取的下一个下标,则直接把该位置的修改为null,然后对下一个可获取的下标进行++操作,如果等于数组长度,则重置为0;
如果指定位置的下标不等于下一个可获取位置的下标,则在一个for循环里,从指定删除位置下标开始,把下一个对象前移一位
这里我们假设目前5个大小的数组是满的,下一个可存放下标重置为0了,然后待移除的下标 i 位置为2,则next=2+1,然后把下标为3的移动到下标为2的,然后把 i=next,循环,然后当i=4的时候,我们的next=5与数组容量一样大小了,这时候把next重置于0,总不能把位置0的前移吧,所以这里需要判断是不是等于下一个可存放位置,如果等于,然后直接把 i位置的置于0,然后把 下一个可存的下标设置为i。等等? 这里是不是正好解决了我们前面哪个问题? 如果我删除了中间一个,则会把下一个可存放重置到最后一个位置。
这里需要注意的是,能够让下一个可取下标变化的操作一定是从数组中删除了这个元素的操作,这样也就避免了我们前面提到的问题,即不存在覆盖的问题。
removeAll是在AbstractCollection抽象类里实现的,通过迭代器操作。
还有最后一个removeIf
也是通过迭代器操作,然后多了一个判断条件,也就是条件过滤。这个predicate仅仅是个接口,目前还没有任何实现,可以通过自己来定义实现,需要重写evaluate方法。有兴趣的可以自己查看。
下面再看看我们别的API的实现。
element方法返回下一个可获取的对象,如果为Null抛出异常
contain(1);返回是否包含这个对象,
arr.drainTo(l); 这个方法参数是一个Collection对象,可以吧队列中的数据批量复制到这个集合中去,
第二个参数是复制多少个,如果不设置默认复制全部,然后删除掉queue中对应的对象。这个有没有一点 批量获取的意思呢?
如果指定第二个参数,则会从可获取下标开始然后复制指定个数的对象。
这里如果我们数组容量为5,批量拉取了前3个,然后后面的会不会前移呢?从源码中看,不会,而且没有必要。我们的下一个存下标和下一个取下标会不停的循环,所以队列即使形成了中空,都是没有关系的。之前有一个ringbuffer的设计,大家可以百度搜的看一看,而且disruptor的ringbuffer设计的更为巧妙。
clear()清空所有
arr.size();返回实际存放的大小
remainingCapacity()返回剩余多少可用容量
arr.toArray();返回一个对象数组
arr.toArray(new Integer[10]);和上面的区别就是如果不指定则返回一个object[]数组,如果指定则返回到你指定的数组中。
arr.spliterator(); 这个是返回一个spliterator,我还没有学习过这个,所以这里暂时不介绍。
因为是基于数组实现的,所以这个的优点不必说,存入和取出都非常快,唯一慢的一个地方就是指定删除。如果数组特别大,比如有1000个容量,然后我们要删除第2个对象,然后就需要把后面的全部前移一位。所以对于有这种需要的需要慎用。
还有一点就是不能扩容,因此初始化时候必须要根据业务初始化为合适大小的队列。
使用场景不必说:
对于快速存快速取,不存在删除中间的需求来说是非常合适的。
如又错误,以及意见建议,欢迎有缘看帖者能够给出批评指正,不甚感激。