面试准备,无法保证理解正确性,慎,欢迎纠正
前言
并发队列与普通队列的区别的确就在于并发二字,而并发的的基础就是线程安全,如何实现线程安全是我们最重要的需要理解的部分.线程安全的保证无非就是可见性和原子性(有序性一般不说).接下来的各种队列主要就这两点的实现来说.
非阻塞队列
非阻塞队列即不使用锁的队列,主要使用CAS操作保证原子性
ConcurrentLinkedQueue
ConcurrentLinkedQueue是无界非阻塞队列.底层数据结构是单向链表,通过volatile分别修饰两个节点,这两个节点分别存放链表的头结点和尾节点来实现可见性,通过CAS操作保证节点入队出队时操作链表的原子性.
-
offer():在队列尾部添加一个元素
- 多线程情况下,如何实现多线程同时插入元素.前面说了,通过CAS实现入队时操作的原子性,看一下源码这一行
if (p.casNext(null, newNode))
,如果多线程同时执行到这一步,因为CAS操作casNext
本身是原子的,如果有一个线程完成了操作,那么其他竞争的线程会重新进入这一行代码的上层循环尝试进行CAS操作,只有成功才会返回. - 这个过程是无锁的,因为没有线程因为没有完成操作而被挂起阻塞,而是在无限循环中不断尝试,就是利用CPU资源换取阻塞引起的开销.孰是孰非,要具体分析.
- 多线程情况下,如何实现多线程同时插入元素.前面说了,通过CAS实现入队时操作的原子性,看一下源码这一行
-
poll() : 从队头移除一个元素
- 还是一行代码:
if (item != null && p.casItem(item, null))
,可以看到,所谓的删除操作就是将当前节点的值设为null,然后重新指定头结点,被移除的节点没了引用,会在gc时被回收,因为整个队列维持了头结点和尾节点两个volatile变量,所以poll和offer并不冲突. - 有一点需要注意,如果没有执行过offer操作就直接poll,会返回null.
- 还是一行代码:
-
peek():获取头结点元素,不移除
- 这个操作其实和poll类似,不过没有cas操作
-
size() ; 获取队列长度
- 这个方法有个问题,因为没有加锁,所以如果在调用size()的过程中可能发生增删的操作,造成统计不准确.
阻塞队列
LinkedBlockingQueue
使用ReentrantLock实现锁机制,底层也是单向列表,也有两个节点存放头节点和尾节点,还有一个count代表元素个数.
-
类中有两个ReentrantLock,分别用于添加和删除操作时的锁控制.LinkedBlockingQueue是一个有界的阻塞队列,可以初始指定容量,默认是0x7fffffff;
-
offer:队列尾部添加一个元素,队列已满返回false,方法不阻塞.
- offer操作的过程是这样的:先判断元素是否为空,为空抛出空指针异常;然后判断队列是否已满,如果已满返回false;构造新节点,获取putLock独占锁,再一次判断队列是否满,不满则入队列,计数+1,最后释放锁.了解了独占锁,这些其实很简单也很正常,解释一下加粗的内容,为什么要重新判断队列是否满.第一次判断队列是否满时还没有拿到独占锁,如果没有拿到独占锁而被挂起,后来再拿到锁时,可能已经有其他线程进入添加了元素,所以要重新判断.
-
put() ; 类似offer,不过如果队列已满不会返回false,而是阻塞线程,直到队列空闲再插入
- 具体实现还有一点需要注意,就是队列已满的等待和没有获取到锁的等待是不同的,前者,会将阻塞线程放到条件变量的条件队列中,后者则是放在AQS的阻塞队列中.具体看ReentrantLock源码那篇
-
poll:从头部移除一个元素,如果队列为空返回null,该方法不阻塞
- 其实实现和offer差不多,不过对应逻辑变一变.注意的是也要判断两次队列是否为空,道理一样.
-
peek():获取头部元素但不删除,队列为空返回null.
-
take():类似poll,不过队列为空阻塞线程直到队列不为空.
-
方法是不是阻塞的,就是当队列满或空时是否阻塞线程.如果被阻塞的线程被其他线程调用了中断方法,会抛出
InterruptedException
异常而返回. -
offer和put操作成功后,会通知被take操作阻塞的线程,类似的,take和poll操作成功后也会通知被put操作阻塞的线程.
ArrayBlockingQueue
底层通过数组实现的有界队列.维持两个下标,一个入队下标,一个出队下标.因为是数组,所以只使用一个独占锁,也就意味着同时只有一个线程进行入队和出队操作.
- offer() ; 向队尾插入一个元素,如果队列有空闲则插入成功并返回true ; 如果队列已满则丢弃当前元素放回false,如果插入元素为null返回空指针异常.
- 如果添加成功,会通知一个被take操作阻塞的线程.put同
- put(); 操作,向队尾插入一个元素,如果队列有空闲则插入成功后直接返回true,如果队列已满则阻塞线程知道队列有空闲并插入成功并返回true;
- poll():从头部移除一个元素,如果队列为空返回null;
- 所谓移除,就是重置对头元素,重设对头下标
- 移除后会激活条件变量通知条件队列中因为队列满而被阻塞的线程.take同
- take();从头部移除一个元素,如果队列空,则阻塞线程.
PriorityBlockingQueue
PriorityBlockingQueue
是一个带优先级的无界阻塞队列,每次出兑都返回优先级最高或者最低的元素,内部采用平衡二叉树堆实现,所以直接遍历队列元素不保证有序,因为是带优先级的,所以队列元素必须实现Comparable
接口,然后设置对象的compareTo
方法,值得一提的是,最大堆还是最小堆是由这个方法决定的;底层采用数组存放元素,
-
设置一个notEmpty条件变量控制删除时的数组为空的情况,维持一个条件队列,当队列中没有元素时,删除操作的线程会被放入这个队列
-
一个很重要的标志
allocationSpinLock
,只有两个状态0-1,0代表数组没有进行扩容,1代表数组正在进行扩容; -
offer(E e) : 在队列中添加一个元素,由于是无界队列,所以只会返回true;方法内部,使用ReetrantLock加锁,当成功加入一个元素后,唤醒notEmpty条件队列中的一个阻塞线程,
- 扩容问题
tryGrow
:PriorityBlockingQueue
为了提高并发性能,使用CAS控制并发操作,而且在执行扩容操作前就释放了offer中添加的独占锁,使得其他线程可以进入,当其他线程拿到锁,进入了offer方法,但是扩容线程还没有完成扩容,就又进入了tryGrow方法,又释放了锁,但是进行CAS失败,不会影响到扩容线程; - 建堆 : 在类中被没有真正的树形的堆,这个最大/最小堆是根据数组存在的,也就是从0-size-1遍历时,对应下标的值就是树形中的层次遍历的顺序,
- 扩容问题
-
poll() : 获取队列内部堆树的根节点元素,如果队列温控,返回null,这个方法不是阻塞的,但是当一处根节点元素后,整个数组需要调整,建立新的堆;
-
put(E e) : 因为是无界的, 所以就是offer
-
take() : 获取队列中根节点的元素,如果队列为空,阻塞线程
-
size( ) : 安全的,内部加锁;