参考链接:深入剖析java并发之阻塞队列LinkedBlockingQueue与ArrayBlockingQueu
ArrayBlockingQueue
1.基于数组实现,保证并发的安全性是基于ReetrantLock和Condition实现的。其中有两个重要的成员变量putindex和takeindex,这两个需要搞懂,putindex就是指向数组中上一个添加完元素的位置的下一个地方,比如刚在index=1的位置添加完,那么putindex就是2,其中有一点特别注意的就是当index=数组的长度减一的时候,意味着数组已经到了满了,那么需要将putindex置位0,原因是数组在被消费的也就是取出操作的时候,是从数组的开始位置取得,所以最开始的位置容易是空的,所以把要添加的位置置位0;takeindex也是一样的,当takeindex到了数组的长度减一的时候,也需要将takeindex置为0。
2.add offer put
add调用了offer方法 add方法数组满了则抛出异常
offer方法:用ReetracLock加锁,首先判断数组是否满了,数组满了则返回false,数组不满的话直接入队,也就是将putindex索引处的值置为新要加入的数,如果加入以后发现putindex++ = 数组的长度,那么说明后面的全部已经填满了,因此putindex置为0,因为前面的可能出队的过程空出来了,所以变为0,最后一步就是执行notEmpty.signal去唤醒消费的执行了take的线程,只有可能是执行了take的方法的线程,因为执行了其它方法remove,poll不会产生线程的挂起操作。
put:首先是ReetrantLock加锁,然后判断是否满了,队列满了,则执行notFull.await()操作挂起,等待notFull.signal()唤醒。没满,则直接进行入队,入队和offer操作一样,也就是将putindex索引处的值置为新要加入的数,如果加入以后发现putindex++ = 数组的长度,那么说明后面的全部已经填满了,因此putindex置为0,因为前面的可能出队的过程空出来了,所以变为0,最后一步就是执行notEmpty()去唤醒消费的执行了take的线程
3.remove,poll,take
poll 首先加锁ReetranLock ,然后判断队列是否为空,不为空,则将putindex出的值用副本copy,然后置位null,然后去执行唤醒notFull()操作,也就是唤醒调用了put操作的线程,唤醒操作并不一定总是发生。
take操作,先加锁,然后如果队列空则notEmpty.await()方法,不为空,则执行和poll一样的出队操作:则将putindex出的值用副本copy,然后置位null,然后去执行唤醒notFull()操作
LinkedBlockingQueue
1.基于链表实现,有takelock和putlock,也就是说可以同时在首尾两端进行操作,因此吞吐量比ArrayBlockingQueue大,同时由于首尾两端都可以进行操作,所以当在进行添加的操作的过程可以一直去添加,直到没有被阻塞的添加线程为止,然后才去执行消费的线程。
1.add,offer,put
add调用offer,满了抛出异常
offer方法 putlock锁,然后不满则加入,同时获取一个c值,c值代表本次队列增加前的队列的数目(一开始长度2,增加1,现在长度是3,那么c就是2),然后判断如果不满则继续去唤醒notFull.signl,去唤醒添加线程去添加(添加过程是直接last节点指向下一个,简单的节点后增加一个节点,然后last指向最后一个节点),上述过程结束,然后去判断 c==0(c代表了之前的队列长度,如果添加之前队列长度是0那么说明可能有挂起的消费线程,需要从队列取元素,但队列长度为0没有元素;判断c>0没有意义,因为添加之前队列不为空,说明不存在挂起的消费线程,挂起的原因是因为队列为空,所以不存在因此源码是判断c==0) ,c如果等于0那么去唤醒阻塞的notEmpty上的条件等待线程。
put 操作就是满了则挂起,不满则执行,同时添加完一个后,发现没满继续去唤醒挂起的添加线程
2.poll take
反之,一样的逻辑
poll 则获取takelcok 然后不为空则出队一个元素,也就是链表的删除头结点操作,通过是如果队列不为空,那么继续去唤醒被挂起的消费线程(消费线程就是执行了队列的take操作的线程),直到没有消费线程或者队列为空,结束,然后如果c(也是队列消费一个头节点的元素后,没消费之前的长度,没发生删除的时候队列的长度),如果c的长度已经是队列的长度,则去唤醒被挂起的执行了put方法的线程,然后释放takelock锁
take方法一样的道理,为空则挂起,不为空一直消费,唤起消费线程一直消费,直到条件不满足,那么去尝试判断c的值,c是队列长度减一,那么去唤醒执行了put方法的被挂起的线程。
以下内容来自 深入剖析java并发之阻塞队列LinkedBlockingQueue与ArrayBlockingQueu
LinkedBlockingQueue和ArrayBlockingQueue迥异
通过上述的分析,对于LinkedBlockingQueue和ArrayBlockingQueue的基本使用以及内部实现原理我们已较为熟悉了,这里我们就对它们两间的区别来个小结
1.队列大小有所不同,ArrayBlockingQueue是有界的初始化必须指定大小,而LinkedBlockingQueue可以是有界的也可以是无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。
2.数据存储容器不同,ArrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockingQueue采用的则是以Node节点作为连接对象的链表。
3.由于ArrayBlockingQueue采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响。
4.两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。