前言
有界并发阻塞队列,基于单向链表的实现,按照FIFO对元素进行操作,默认和最大长度都为Integer.MAX_VALUE;不允许null元素添加;基于链表的队列的吞吐量要高于基于数组的队列如ArrayBlockingQueue,但是高并发环境下性能较低。在高并发环境下并发队列建议使用ConcurrentLinkedQueue,非阻塞算法能提供更高的并发性能。
定义
需要注意LinkedBlockingQueue实现了并发包中BlockingQueue接口,这个接口是并发阻塞队列接口。
重要的字段
Node为静态单向链表的节点类,定义比较简单;head节点一定满足条件 head.item == null,所以head节点并不存储元素,是哨兵节点;last节点满足 last.next == null。需要注意的是Node类和head节点都是默认访问权限(向包内类开放)。
队列容量默认为Integer.MAX_VALUE,元素数量计数使用的AtomicInteger来实现线程安全的增加减少元素数量。
了解这上面的锁和条件等到变量就基本掌握了阻塞队列的实现原理了。需要注意的是使用的是ReentrantLock默认构造器,是非公平锁,所以并不能保证阻塞时间更长的线程更早获取锁,而且ReentrantLock是独占重入锁。
构造函数
无参构造构造器,默认容量Integer.MAX_VALUE
指定容量
默认容量,集合构造器
注意此构造器并不会发生竞争冲突,但是还是加锁了,利用AQS(volatile+CAS)中提供锁的语义,使得添加到队列中的元素在锁释放后对其他线程可见(JMM中相当于刷新到主内存中)。
put方法(响应中断)
put方法响应中断是通过重入锁ReentrantLock.lockInterruptibly以及Condition.await来实现的,不允许添加的元素为null
offer方法(中断超时)
经典的超时等待设计,在指定的时间内插入元素到队列的末尾,如果超时则返回false,如果在获取锁和或者调用await被阻塞时通过自身的中断来响应外部的中断通知。
offer方法(不响应中断,纯阻塞)
此方法不响应中断,直到获取到锁才能从此方法返回,在方法内部在获取锁后,如果容量已满则直接返回,没有等待。
take(响应中断)
返回删除的元素,需要注意的是dequeue方法,将first设置成自连接节点来使得删除的节点被GC,当遍历到这个节点时,说明这个节点已经被删除了,需要重新从head开始遍历。
poll(中断超时)
返回删除的元素
poll(不响应中断,纯阻塞)
返回删除的元素
peek
通过加锁,来查看查看队列的第一个有效元素first(head.next)
remove(Object o)
remove方法需要锁住队列两端防止任何修改操作执行,遍历整个队列找到相等的元素(equals)然后删除,时间复杂度为O(n)。
contains(Object o)
同样需要锁住两端,遍历队列,时间复杂度为O(n)。
size
获取当前的队列中元素的数量。
iterator 迭代器设计
next方法获取当前游标的队列元素时需要双端加锁,还得跳过已经被删除的元素。
注意这个remove方法,同样是双端加锁,但是遍历整个队列寻找上一次next方法返回的元素所在的Node节点,并进行删除。
LinkedBlockingQueue的iterator 迭代器是线程安全的,所以并不会抛出ConcurrentModificationException异常。但是并不是强一致性的。
常用方法列表
总结
- 基于非公平独占锁ReentrantLock来保证线程安全性和可见性,双端都有锁,put锁和take锁,当发生竞争则会阻塞线程;而ConcurrentLinkedQueue是通过volatile+循环CAS保证线程安全性以及可见性,并不会阻塞线程。
- 通过AtomicInteger来统计队列的元素数量,保证了线程安全性和实时性。size方法返回当前线程正在持有的元素数量。而ConcurrentLinkedQueue,size需要无锁遍历队列,并且是不精确的。
- 迭代器方法和ConcurrentLinkedQueue一样都是弱一致性的。
- 队列的头尾操作时间复杂度都是O(1),查找删除与指定元素相等的队列中的元素时间复杂度都为O(n);但是LinkedBlockingQueue.size时间复杂度为O(1),ConcurrentLinkedQueue.size时间复杂度为O(n),