关于java-ee:JAVA中常见的阻塞队列详解

8次阅读

共计 5765 个字符,预计需要花费 15 分钟才能阅读完成。

  • 在之前的线程池的介绍中咱们看到了很多阻塞队列,这篇文章咱们次要来说说阻塞队列的事。
  • 阻塞队列也就是 BlockingQueue,这个类是一个接
  • 口,同时继承了 Queue 接口,这两个接口都是在JDK5 中退出的。
  • BlockingQueue 阻塞队列是线程平安的,在咱们业务中是会常常频繁应用到的,如典型的生产者生产的场景,生产者只须要向队列中增加,而消费者负责从队列中获取。

  • 如上图展现,咱们生产者线程一直的put 元素到队列,而消费者从中take 出元素解决,这样实现了工作与执行工作类之间的解耦,工作都被放入到了阻塞队列中,这样生产者和消费者之间就不会间接互相拜访实现了隔离进步了安全性。

并发队列

  • 下面是 Java 中队列 Queue 类的类图,咱们能够看到它分为两大类, 阻塞队列与非阻塞队列
  • 阻塞队列的实现接口是 BlockingQueue 而非阻塞队列的接口是 ConcurrentLinkedQueue , 本文次要介绍阻塞队列,非阻塞队列不再过多论述
  • BlockingQueue 次要有上面六个实现类,别离是 ArrayBlockingQueueLinkedBlockingQueueSynchronousQueueDelayQueuePriorityBlockingQueueLinkedTransferQueue。这些阻塞队列有着各自的特点和实用场景,前面具体介绍。
  • 非阻塞队列的典型例子如 ConcurrentLinkedQueue , 它不会阻塞线程,而是利用了 CAS 来保障线程的平安。
  • 其实还有一个队列和 Queue 关系很严密,那就是Deque,这其实是 double-ended-queue 的缩写,意思是双端队列。它的特点是从头部和尾部都能增加和删除元素,而咱们常见的一般队列Queue 则是只能一端进一端出,即FIFO

阻塞队列特点

  • 阻塞队列的特点就在于阻塞,它能够阻塞线程,让生产者消费者得以均衡,阻塞队列中有两个要害办法 PutTake 办法

take 办法

  • take 办法的性能是获取并移除队列的头结点,通常在队列里有数据的时候是能够失常移除的。可是一旦执行 take 办法的时候,队列里无数据,则阻塞,直到队列里有数据。一旦队列里有数据了,就会立即解除阻塞状态,并且取到数据。过程如图所示:


put 办法

  • put 办法插入元素时,如果队列没有满,那就和一般的插入一样是失常的插入,然而如果队列已满,那么就无奈持续插入,则阻塞,直到队列里有了闲暇空间。如果后续队列有了闲暇空间,比方消费者生产了一个元素,那么此时队列就会解除阻塞状态,并把须要增加的数据增加到队列中。过程如图所示:


是否有界(容量有多大)

  • 此外,阻塞队列还有一个十分重要的属性,那就是容量的大小,分为有界和无界两种。
  • 无界队列意味着外面能够包容十分多的元素,例如 LinkedBlockingQueue 的下限是 Integer.MAX_VALUE,约为 2 的 31 次方,是十分大的一个数,能够近似认为是有限容量,因为咱们简直无奈把这个容量装满。
  • 然而有的阻塞队列是有界的,例如 ArrayBlockingQueue 如果容量满了,也不会扩容,所以一旦满了就无奈再往里放数据了。

阻塞队列常见办法

  • 首先咱们从罕用的办法登程,依据各自的特点咱们能够大抵分为三个大类,如下表所示:
分类 办法 含意 特点
抛出异样 add 增加一个元素 如果队列已满,增加则抛出  IllegalStateException 异样
remove 删除队列头节点 当队列为空后,删除则抛出  NoSuchElementException 异样
element 获取队列头元素 当队列为空时,则抛出 NoSuchElementException 异样
返回无异样 offer 增加一个元素 当队列已满,不会报异样,返回  false,如果胜利返回 true
poll 获取队列头节点,并且删除它 当队列空时,返回  Null  
peek 单纯获取头节点 当队列为空时反馈 NULL
阻塞 put 增加一个元素 如果队列已满则阻塞
take 返回并删除头元素 如果队列为空则阻塞
  • 如下面所示次要的八个办法,绝对都比较简单,上面咱们通过理论代码演示的形式来意识

抛异样类型[add、remove、element]

add

  • 向队列中增加一个元素。如果队列是有界队列,当队列已满时再增加则抛出异样提醒,如下:
        BlockingQueue queue = new ArrayBlockingQueue(2);
        queue.add(1);
        queue.add(2);
        queue.add(3);
  • 上述代码中咱们创立了一个阻塞队列容量为 2,当咱们应用 add 向其中增加元素,当增加到第三个时则会抛出异样如下:

remove

  • remove 办法是从队列中删除队列的头节点,同时会返回该元素。当队列中为空时执行 remove 办法时则会抛出异样,代码如下:
    private static void groupRemove() {BlockingQueue queue = new ArrayBlockingQueue(2);
        queue.add("i-code.online");
        System.out.println(queue.remove());
        System.out.println(queue.remove());
    }
  • 上述代码中,咱们能够看到,咱们想队列中增加了一个元素 i-code.online , 之后通过 remove 办法进行删除,当执行第二次remove 时队列内已无元素,则抛出异样。如下:

element

  • element 办法是获取队列的头元素,然而并不是删除该元素,这也是与 remove 的区别,当队列中没有元素后咱们再执行 element 办法时则会抛出异样,代码如下:
    private static void groupElement() {BlockingQueue queue = new ArrayBlockingQueue(2);
        queue.add("i-code.online");
        System.out.println(queue.element());
        System.out.println(queue.element());
    }
    private static void groupElement2() {BlockingQueue queue = new ArrayBlockingQueue(2);
        System.out.println(queue.element());
    }
  • 下面两个办法别离演示了在有元素和无元素的状况element 的应用。在第一个办法中并不会报错,因为首元素始终存在的,第二个办法中因为空的,所以抛出异样,如下后果:

无异样类型[offer、poll、peek]

offer

  • offer 办法是向队列中增加元素, 同时反馈胜利与失败,如果失败则返回 false,当队列已满时持续增加则会失败,代码如下:
    private static void groupOffer() {BlockingQueue queue = new ArrayBlockingQueue(2);
        System.out.println(queue.offer("i-code.online"));
        System.out.println(queue.offer("云栖简码"));
        System.out.println(queue.offer("AnonyStar"));
    }
  • 如上述代码所示,咱们向一个容量为 2 的队列中通过offer 增加元素,当增加第三个时,则会反馈 false,如下后果:

true
true
false

poll

  • poll 办法对应下面 remove 办法,两者的区别就在于是否会在无元素状况下抛出异样,poll 办法在无元素时不会抛出异样而是返回null,如下代码:
    private static void groupPoll() {BlockingQueue queue = new ArrayBlockingQueue(2);
        System.out.println(queue.offer("云栖简码")); // 增加元素
        System.out.println(queue.poll()); // 取出头元素并且删除
        System.out.println(queue.poll());

    }
  • 下面代码中咱们创立一个容量为 2 的队列,并增加一个元素,之后调用两次 poll 办法来获取并删除头节点,发现第二次调用时为null,因为队列中曾经为空了,如下:

true
云栖简码
null

peek

  • peek 办法与后面的 element 办法是对应的,获取元素头节点但不删除,与其不同的在于peek 办法在空队列下并不会抛出异样,而是返回 null,如下:
    private static void groupPeek() {BlockingQueue queue = new ArrayBlockingQueue(2);
        System.out.println(queue.offer(1));
        System.out.println(queue.peek());
        System.out.println(queue.peek());
    }
    private static void groupPeek2() {BlockingQueue queue = new ArrayBlockingQueue(2);
        System.out.println(queue.peek());
    }
  • 如上述代码所示,我么们别离展现了非空队列与空队列下peek 的应用,后果如下:

阻塞类型[put、take]

put

  • put 办法是向队列中增加一个元素,这个办法是阻塞的,也就是说当队列曾经满的状况下,再 put 元素时则会阻塞,直到队列中有空位.

take

  • take 办法是从队列中获取头节点并且将其移除,这也是一个阻塞办法,当队列中曾经没有元素时,take 办法则会进入阻塞状态,直到队列中有新的元素进入。

常见的阻塞队列

ArrayBlockingQueue

  • ArrayBlockingQueue 是一个咱们罕用的典型的 有界队列,其外部的实现是基于数组来实现的,咱们在创立时须要指定其长度,它的线程安全性由 ReentrantLock 来实现的。
public ArrayBlockingQueue(int capacity) {...}
public ArrayBlockingQueue(int capacity, boolean fair) {...}
  • 如上所示,ArrayBlockingQueue 提供的构造函数中,咱们须要指定队列的长度,同时咱们也能够设置队列是都是偏心的,当咱们设置了容量后就不能再批改了,合乎数组的个性,此队列依照先进先出(FIFO)的准则对元素进行排序。
  • ReentrantLock 一样,如果 ArrayBlockingQueue 被设置为非偏心的,那么就存在插队的可能;如果设置为偏心的,那么期待了最长工夫的线程会被优先解决,其余线程不容许插队,不过这样的偏心策略同时会带来肯定的性能损耗,因为非偏心的吞吐量通常会高于偏心的状况。

LinkedBlockingQueue

  • 从它的名字咱们能够晓得,它是一个由链表实现的队列,这个队列的长度是 Integer.MAX_VALUE,这个值是十分大的,简直无奈达到,对此咱们能够认为这个队列根本属于一个无界队列(也又认为是有界队列)。此队列依照先进先出的程序进行排序。

SynchronousQueue

  • synchronousQueue 是一个不存储任何元素的阻塞队列,每一个 put 操作必须期待 take 操作,否则不能增加元素。同时它也反对偏心锁和非偏心锁。
  • synchronousQueue 的容量并不是 1,而是 0。因为它自身不会持有任何元素,它是间接传递的,synchronousQueue 会把元素从生产者间接传递给消费者,在这个过程中可能是不须要存储的
  • 在咱们之前介绍过的线程池 CachedThreadPool 就是利用了该队列。Executors.newCachedThreadPool(),因为这个线程池它的最大线程数是Integer.MAX_VALUE,它是更具需要来创立线程,所有的线程都是长期线程,应用完后闲暇 60 秒则被回收,

PriorityBlockingQueue

  • PriorityBlockingQueue 是一个反对优先级排序的无界阻塞队列,能够通过自定义实现 compareTo() 办法来指定元素的排序规定,或者通过结构器参数 Comparator 来指定排序规定。然而须要留神插入队列的对象必须是可比拟大小的,也就是 Comparable 的,否则会抛出 ClassCastException 异样。
  • 它的 take 办法在队列为空的时候会阻塞,然而正因为它是无界队列,而且会主动扩容,所以它的队列永远不会满,所以它的 put 办法永远不会阻塞,增加操作始终都会胜利

DelayQueue

  • DelayQueue 是一个实现 PriorityBlockingQueue 的提早获取的无界队列。具备“提早”的性能。
  • DelayQueue 利用场景:1. 缓存零碎的设计:能够用 DelayQueue 保留缓存元素的有效期,应用一个线程循环查问 DelayQueue,一旦能从DelayQueue 中获取元素时,示意缓存有效期到了。2. 定时任务调度。应用 DelayQueue 保留当天将会执行的工作和执行工夫,一旦从 DelayQueue 中获取到工作就开始执行,从比方 TimerQueue 就是应用 DelayQueue 实现的。
  • 它是无界队列,放入的元素必须实现 Delayed 接口,而 Delayed 接口又继承了 Comparable 接口,所以天然就领有了比拟和排序的能力,代码如下:
public interface Delayed extends Comparable<Delayed> {long getDelay(TimeUnit unit);
}
  • 能够看出 Delayed 接口继承 Comparable,外面有一个须要实现的办法,就是  getDelay。这里的 getDelay 办法返回的是“还剩下多长的延迟时间才会被执行”,如果返回 0 或者正数则代表工作已过期。
  • 元素会依据延迟时间的长短被放到队列的不同地位,越凑近队列头代表越早过期。

本文由 AnonyStar 公布, 可转载但需申明原文出处。
欢送关注微信公账号:云栖简码 获取更多优质文章
更多文章关注笔者博客:云栖简码 i-code.online

正文完
 0