LinkedBlockingQueue 需要掌握的知识点
- 添加的三种方法的实现方式和区别:
add
、offer
、put
- 移除的三种方法的实现方式和区别:
remove
、poll
、take
添加元素
1. offer()
方法
我们来梳理一下【图1】中的offer方法所做的事情。
- 【步骤一】首先第一次判断队列是否已满,如果已经满了,则直接返回false
- 【步骤二】如果第一次判断队列没有满,则对队列加锁
putLock
,加锁后第二次判断队列有没有满,因为这里存在高并发的情况,所以需要第二次判断队列是否满。 - 【步骤三】如果第二次判断队列还是没有满,则向队列中加入结点。
- 【步骤四】加入节点后,判断加入结点之后队列的长度是否超过了队列长度的限制(
Integer.MAX_VALUE
),如果没有超过则唤醒下一个添加元素的线程。 - 【步骤五】这里在【第442行】判断了
if(c == 0)
,如果 c 的值等于 0 说明队列中还有1条数据没有被消费,所以要唤醒消费线程,即调用signalNotEmpty
方法。 (注意:这里为什么是 c=0 指还有一条数据没被消费,因为看【第420行】c 的初始值是 -1 )
这里需要注意的两点。
-
唤醒添加线程的原因(【第435行】)。在添加元素完成后,会判断队列是否已经满了,如果队列不满就会继续唤醒在条件对象 notFull 上添加线程,注意这里与 ArrayBlockingQueue 的操作不一样。
- 在ArrayBlockingQueue 内部完成添加元素的操作之后,会直接唤醒消费线程对元素进行获取,之所以这样操作,是因为 ArrayBlockingQueue 使用一个 ReentrantLock 对消费者和生产者进行控制,使用2个Condition进行消费和生成操作,在ArrayBlockingQueue中如果在添加完成后再次唤醒添加线程的话,消费线程可能永远都无法执行。
- 在LinkedBlockingQueue中,生产者和消费者是不互斥的,即消费元素和生产元素可以同时进行。生产者线程直接唤醒生产者的其他线程,如果没有等待的生产者,则直接返回。如果有就等到所有生产者都执行完再结束。
-
为什么判断if(c==0)时才去唤醒消费线程,因为消费线程一旦被唤醒就一直进行消费,所以c的值一直在不断变化,c值是添加完元素时队列的大小,这个时候c的值可能是 0 或者 大于0。如果 c=0,说明之前消费线程已经停止了,这多出的一个元素是刚刚生产出来的,所以需要调用消费者线程进行消费。如果c>0,那么消费线程不会被唤醒,只能等待下一个消费操作(take、poll、remove等),为什么 c>0 时,消费线程不会被唤醒呢? 因为 c>0 是上次调用的消费线程还没有消费完队列中的数据。
删除元素
1. remove
方法
【注】trail 是 p 元素的前一个元素。
这里为什么要将 生产者 和 消费者 同时加锁呢?我的理解是,因为remove方法删除元素的位置是不确定的,如果恰好删除的元素在队头或者队尾,这样在队尾添加元素的时候会出现并发的问题。
2. poll
方法
这个操作与 add
操作相类似,这里不再赘述。
3. take
方法
take 方法与poll的主要区别就是,take方法是可相应中断的方法,并且如果队列为空的话,就一直循环等待直到队列中有元素为止。
总结
LinkedBlockingQueue
是一个单向链表、范围任意的、FIFO阻塞队列;访问和移除元素在队头,添加元素在队尾。
LinkedBlockingQueue
与 ArrayBlockingQueue
的区别
- 【区别一】队列大小有所不同,
ArrayBlockingQueue
是有界的初始化必须指定大小,而LinkedBlockingQueue
可以是有界的也可以是无界的(Integer.MAX_VALUE
),对于后者而言,当增加速度大于移动速度时,在无界的情况下,可能会造成内存溢出等问题。 - 【区别二】数据存储容器不同,
ArrayBlockingQueue
采用的是数组作为数据存储容器,而LinkedBlockingQueue
采用的则是以Node节点作为连接对象的链表。 - 【区别三】由于
ArrayBlockingQueue
采用的是数组的存储容器,因此在插入或者删除元素时不会产生或者销毁任何额外的独享实例,而LinkedBlockingQueue
则会产生额外的Node
对象。这可能在长时间内需要搞笑并发地处理大批量数据时,对GC
可能存在较大的影响。 - 【区别四】两者的实现队列添加或者移除的锁不一样,
ArrayBlockingQueue
实现的队列中的锁是没有分离的,即添加移动和移除操作采用的都是同一个ReentrantLock
锁,而LinkedBlockingQueue
实现的队列中的锁时分离的,其添加采用的是putLock
,移除采用的是takeLock
,这样可以提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
参考并感谢
[1] https://blog.youkuaiyun.com/javazejian/article/details/77410889