简简单单的阻塞,非阻塞队列

什么是队列

队列是一种非常常用的数据结构,一进一出,先进先出。

 图片1.png

阻塞队列与非阻塞队列

非阻塞队列与阻塞队列,当然它们都是线程安全的,无需担心在多线程并发环境所带来的不可预知的问题。为什么会有非阻塞和阻塞之分呢?我们来假设在现在有一个有界的队列。

有界非阻塞队列

如果队列满的时候,像尾部再次进行插入会立刻返回插入失败,并且无视此次插入。

如果队列空的时候,移除并获取队列首个元素会立刻返回移除失败,并无视此次移除操作。

有界阻塞队列

如果队列满的时候,像尾部再次进行插入会将该线程阻塞,直到队列中元素出队,则线程继续执行。当然,如果队列持续没有空位给我们插入,我们可以选择超时等待机制,采用类似有界队列的处理方式,当超过我们设置的固定时长依旧没有插入成功的话,则返回插入失败,线程继续执行。

如果队列空的时候,移除并获取队列首个元素会将该线程阻塞,直到队列中有新的元素插入。同理,如果队列持续没有新的元素加入进来,我们也可以选择超时等待机制。当超过我们设置的固定时长依旧没有移除并获取成功的话,则返回null,否则返回取得的元素,线程继续执行。

对比总结

我们发现,实际上阻塞队列和非阻塞队列实际上的区别,就是在有超出其容量的操作时,选择的拒绝策略不同。非阻塞队列采用直接拒绝并无视的策略,而阻塞队列则可以有强行等待(保证数据绝对的安全性),以及超时等待(保证线程不会过长时间的阻塞)两种拒绝方案。

队列的常见方法

非阻塞队列中的几个主要方法

  1. add(E e):将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则会抛出异常;
  2. remove():移除队首元素,若移除成功,则返回true;如果移除失败(队列为空),则会抛出异常;
  3. offer(E e):将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则返回false;
  4. poll():移除并获取队首元素,若成功,则返回队首元素;否则返回null;
  5. peek():获取队首元素,若成功,则返回队首元素;否则返回null

注:对于非阻塞队列,一般情况下建议使用offer、poll和peek三个方法,不建议使用add和remove方法。因为使用offer、poll和peek三个方法可以通过返回值判断操作成功与否,而使用add和remove方法却不能达到这样的效果。

阻塞队列中的几个主要方法

  1. put(E e) : 用来向队尾存入元素,如果队列满,则等待;
  2. take() : 用来从队首取元素,如果队列为空,则等待;
  3. offer(E e,long timeout, TimeUnit unit) : 方法用来向队尾存入元素,如果队列满,则等待一定的时间,当时间期限达到时,如果还没有插入成功,则返回false;否则返回true;
  4. poll(long timeout, TimeUnit unit) : 方法用来从队首取元素,如果队列空,则等待一定的时间,当时间期限达到时,如果取到,则返回null;否则返回取得的元素;

队列的重要子类

继承关系图

image.png

阻塞队列子类介绍

ArrayBlockingQueue

用数组实现的有界阻塞队列,默认情况下不保证线程公平的访问队列(按照阻塞的先后顺序访问队列),队列可用的时候,阻塞的线程都可以争夺队列的访问资格,当然也可以使用以下的构造方法创建一个公平的阻塞队列。ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(10, true)。(其实就是通过将ReentrantLock设置为true来 达到这种公平性的:即等待时间最长的线程会先操作)。用ReentrantLock condition 实现阻塞。

LinkedBlockingQueue

基于链表实现的有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE(默认还是无界队列)。此队列按照先进先出的原则对元素进行排序。这个队列的实现原理和ArrayBlockingQueue实现基本相同。也是采用ReentrantLock 控制并发,那么具体和ArrayBlockingQueue有什么区别呢?

ArrayBlockingQueue和LinkedBlockingQueue的区别

队列中锁的实现不同(高并发环境下效率提升明显)

ArrayBlockingQueue实现的队列中的锁是没有分离的,即生产和消费用的是同一个锁;

LinkedBlockingQueue实现的队列中的锁是分离的,即生产用的是putLock,消费是takeLock

在生产或消费时操作不同(每一次插入和删除稍微影响性能)

ArrayBlockingQueue实现的队列中在生产和消费的时候,是直接将枚举对象插入或移除的;

LinkedBlockingQueue实现的队列中在生产和消费的时候,需要把枚举对象转换为Node进行插入或移除,会影响性能

队列大小初始化方式不同

ArrayBlockingQueue实现的队列中必须指定队列的大小;

LinkedBlockingQueue实现的队列中可以不指定队列的大小,但是默认是Integer.MAX_VALUE

总结

可以看到,由于阻塞队列本身在取值方面,每次都取的首个元素,原本数组出色的遍历查询特性反而无永无之地了。在并发环境不高的情况下,ArrayBlockingQueue因为枚举的直接插入和移除,因此效率要强于LinkedBlockingQueue将枚举封装为Node再进行插入和移除。但是随着并发量的不断增大,LinkedBlockingQueue因为锁分离的机制,必然速度要远强于ArrayBlockingQueue。

PriorityBlockingQueue

priorityBlockingQueue是一个无界队列,它没有限制,在内存允许的情况下可以无限添加元素;它又是具有优先级的队列,是通过构造函数传入的对象来判断,传入的对象必须实现comparable接口。需要注意的是不能保证同优先级元素的顺序。

DelayQueue

DelayQueue是在PriorityQueue基础上实现的,底层也是数组构造方法,是一个存放Delayed 元素的无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,并且poll将返回null。当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于或等于零的值时,则出现期满,poll就移除这个元素了。此队列不允许使用 null 元素。

可以把DelayQueue想象成一个监狱,每一个进去的元素,都要等待刑满后才可以释放。

DelayQueue非常有用。可以将DelayQuue运用在以下应用场景。

缓存系统的设计:可以用DelayedQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue。一旦能从DelayQueue中获取元素时,表示缓存有效期到了。还有订单到期,限时支付等等。

SychronousQueue

一个没有容量的队列 ,不会存储数据,每执行一次put就要执行一次take,否则就会阻塞。未使用锁。通过cas实现,吞吐量异常高。内部采用的就是ArrayBlockingQueue的阻塞队列,所以在功能上完全可以用ArrayBlockingQueue替换,但是SynchronousQueue是轻量级的,SynchronousQueue不具有任何内部容量,我们可以用来在线程间安全的交换单一元素。所以功能比较单一,优势就在于轻量。

SynchronousQueue的队列长度为0,最初我认为这好像没多大用处,但后来我发现它是整个Java Collection Framework中最有用的队列实现类之一,特别是对于两个线程之间传递元素这种用例。

LinkedBlockingDeque

我们发现,这个阻塞队列很特殊,别的阻塞队列都是以Queue结尾,它确实以Deque结尾的。

LinkedBlockingDeque是双向链表实现的双向并发阻塞队列(多了 addFirst、addLast、offerFirst、offerLast、peekFirst 和 peekLast 等方法)。

该阻塞队列同时支持FIFO和FILO两种操作方式,即可以从队列的头和尾同时操作(插入/删除);并且,该阻塞队列是支持线程安全,当多线程竞争同一个资源时,某线程获取到该资源之后,其它线程需要阻塞等待。此外,LinkedBlockingDeque还是可选容量的(防止过度膨胀),即可以指定队列的容量。如果不指定,默认容量大小等于Integer.MAX_VALUE。在初始化LinkedBlockingDeque时可以设置容量方法其过度膨胀。另外,双向阻塞队列可以运用在”工作窃取”模式中。

非阻塞队列子类介绍

PriorityQueue

回到我们之前讲的PriorityBlockingQueue。PriorityBlockingQueue是一个带优先级的队列,而不是先进先出队列。元素按优先级顺序被移除,该队列也没有上限(PriorityBlockingQueue是对 PriorityQueue的再次包装,是基于堆数据结构的,而PriorityQueue是没有容量限制的,与ArrayList一样,所以在优先阻塞 队列上put时是不会受阻的。虽然此队列逻辑上是无界的,但是由于资源被耗尽,所以试图执行添加操作可能会导致 OutOfMemoryError)。

因此,为了让数据不再出现OOM这种问题,才在非阻塞队列PriorityQueue的基础上进行了加强,设计了PriorityBlockingQueue,它的检索操作take是受阻的。也是用ReentrantLock控制并发。

ConcurrentLinkedQueue

ConcurrentLinkedQueue是一个基于链表的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部;当我们获取一个元素时,它会返回队列头部的元素。ConcurrentLinkedQueue的线程安全是通过其插入、删除时采取CAS操作来保证的。由于使用CAS没有使用锁,所以获取size的时候有可能正在进行offer,poll或者remove操作,导致获取的元素个数不精确(和ConcurrentHashMap一样的弱一致性),所以在并发情况下size函数不是很有用。

因此,在使用的角度,我们可以把它当做一个高效的,线程安全的LinkedList。

LinkedTransferQueue(容易被忽略的强大队列)

LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法。

从执行效率上来说:LinkedTransferQueue采用一种预占模式。意思就是消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,那就生成一个节点(节点元素为null)入队,然后消费者线程被等待在这个节点上,后面生产者线程入队时发现有一个元素为null的节点,生产者线程就不入队了,直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素,从调用的方法返回。我们称这种节点操作为“匹配”方式。

LinkedTransferQueue的生产者会一直阻塞直到所添加到队列的元素被某一个消费者所消费(不仅仅是添加到队列里就完事)。新添加的transfer方法用来实现这种约束。顾名思义,阻塞就是发生在元素从一个线程transfer到另一个线程的过程中,它有效地实现了元素在线程之间的传递(以建立Java内存模型中的happens-before关系的方式)。

TransferQueue相比SynchronousQueue用处更广、更好用,因为你可以决定是使用BlockingQueue的方法(译者注:例如put方法)还是确保一次传递完成(译者注:即transfer方法)。在队列中已有元素的情况下,调用transfer方法,可以确保队列中被传递元素之前的所有元素都能被处理。它不仅仅综合了这几个类的功能,同时也提供了更高效的实现(匹配方式)。

猜你喜欢

转载自blog.csdn.net/weixin_47184173/article/details/115267958