多线程7-阻塞队列

Posted zhangblearn

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程7-阻塞队列相关的知识,希望对你有一定的参考价值。

什么是阻塞队列?

  一个支持两个附加操作的队列。这两个附加操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者场景。


 非阻塞队列与阻塞队列处理方法对比:

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

    add(E e):将元素e插入到队列末尾。成功返回true,失败(队列已满)抛出异常。

    remove(e):移除队首元素,成功返回true,失败(队列为空)抛出异常。

    offer(E e):将元素e插入到队列末尾,成功返回true,失败(队列已满)返回false。

    poll():移除并获取队首元素,成功返回队首元素,失败返回null。

    peek():获取队首元素,成功返回队首元素,失败返回null。

  一般建议在使用非阻塞队列时,使用offer、poll、peek。因为这三个方法能通过返回值判断操作成功与否。非阻塞队列中的方法都没有进进行同步措施。

  阻塞队列包括了非阻塞队列中的大部分方法,上面五个都存在其中。但在阻塞队列中都进行了同步措施。除此之外阻塞队列中还有几个非常有用的方法:

    put(E e):向队尾存入元素,队列满则等待。

    take():获取队首元素,队列为空则等待。

    offer():向队尾存入元素,队列满则等待一定时间,到达时间期限时,如果还未插入成功则返回false,否则返回true。

    poll():获取队首元素,队列为空则等待一定时间,到达时间期限时,如果未取到则返回null,否则返回取得的元素。


Java中提供的7个阻塞队列:  

  ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。

  LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。默认和最大长度为Integer.MAX_VALUE。按照先进先出的原则对元素进行排序。

  PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。默认下元素采取自然顺序排序,也可通过比较器comparator来指定元素的排序规则,按升序排序。

  DelayQueue:一个使用优先级队列实现的无界阻塞队列。

  SynchronousQueue:一个不存储元素的阻塞队列。每一个put操作必须等到一个take操作。否则不能继续添加元素。适用于传递性场景。

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

  LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。


 ArrayBlockingQueue:

  一个数组实现的有界阻塞队列。按先进先出(FIFO)原则对元素进行排序。默认不保证访问者公平的访问队列。

公平访问队列:
    阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素

 

  我们可以ArrayBlockingQueue建立一个公平的阻塞队列,其通过可重入锁实现访问者的公平性

ArrayBlockingQueue fairQueue = new  ArrayBlockingQueue(1000,true);

 DelayQueue:

  支持延时获取元素的无界阻塞队列。使用PriorityQueue来实现。队列中的元素必须实现Delayed接口。创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。

DelayQueue常见运用的应用场景:
    1、缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了
    2、定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的

 LinkedTransferQueue:

  transfer方法:

    如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回。

  tryTransfer方法:

    试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回。而transfer方法是必须等到消费者消费了才返回。对于带有时间限制的tryTransfer(E e, long timeout, TimeUnit unit)方法,则是试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回false,如果在超时时间内消费了元素,则返回true。


 LinkedBlockingDeque:

  可以从队列两端插入和移出元素。双端队列因其多了一个操作队列的入口。多线程同时入队时,减少了一半的竞争。相比其他队列,LinkedBlockingDeque多了addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast等方法,以First单词结尾的方法,表示插入,获取(peek)或移除双端队列的第一个元素。以Last单词结尾的方法,表示插入,获取或移除双端队列的最后一个元素。另外插入方法add等同于addLast,移除方法remove等效于removeFirst。但是take方法却等同于takeFirst,不知道是不是Jdk的bug,使用时还是用带有First和Last后缀的方法更清楚。在初始化LinkedBlockingDeque时可以初始化队列的容量,用来防止其再扩容时过渡膨胀。另外双向阻塞队列可以运用在“工作窃取”模式中。


 阻塞队列的实现原理:

  我们采用ArrayBlockingQueue为例。先看一下其中的几个成员变量

 //用以存储元素的实际是一个数组
final Object[] items; 
//队首元素下标
int takeIndex;
//队尾元素下标
int putIndex;
//队列中元素个数
int count;
 //可重入锁
final ReentrantLock lock;
//等待条件
private final Condition notEmpty;
private final Condition notFull;

 

  构造器:

//capacity指定容量
public ArrayBlockingQueue(int capacity) {}
//指定容量和公平性
public ArrayBlockingQueue(int capacity, boolean fair) {}
//指定容量、公平性及用另一个集合进行初始化
public ArrayBlockingQueue(int capacity, boolean fair,
                          Collection<? extends E> c) {
}

  看一下put和take方法的源码,了解一下实现:

public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
     //获取可中断锁 lock.lockInterruptibly();
try {
       //当前元素个数等于数组长度
while (count == items.length)
          //等于则让线程等待 notFull.await();
        //不等于则存入元素 enqueue(e); }
finally { lock.unlock(); } } //checkNotNull 校验元素是否为null,是抛出异常 private static void checkNotNull(Object v) { if (v == null) throw new NullPointerException(); } //enqueue 存入元素 private void enqueue(E x) { final Object[] items = this.items; items[putIndex] = x; if (++putIndex == items.length) putIndex = 0; count++;
     //存入成功后,唤醒取元素线程 notEmpty.signal(); }

   take方法实现:

public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
      //获取可中断锁 lock.lockInterruptibly();
try {
        //当元素个数为0
while (count == 0)
          //暂停线程 notEmpty.await();
        //不为0,获取元素
return dequeue(); } finally { lock.unlock(); } } private E dequeue() { final Object[] items = this.items; @SuppressWarnings("unchecked") E x = (E) items[takeIndex]; items[takeIndex] = null; if (++takeIndex == items.length) takeIndex = 0; count--; if (itrs != null) itrs.elementDequeued();
      //获取成功,唤醒插入线程 notFull.signal();
return x; }

  take方法与put方法实现相似,put方法等待的是notFull信号,而take方法等待的是notEmpty信息。阻塞队列与我们利用wait/notify和非阻塞队列实现生产者-消费者思路类似。

  阻塞队列常用场景是socket客户端数据读取和解析。读取数据的线程不断将数据放入队列。解析线程不断从队列取数据解析。

 











以上是关于多线程7-阻塞队列的主要内容,如果未能解决你的问题,请参考以下文章

线程队列之阻塞队列LinkedBlockingQueue

多线程(七):单例模式+阻塞式队列

多线程(七):单例模式+阻塞式队列

多线程案例 -- 单例模式阻塞队列

JavaWeb 基础知识 --多线程(阻塞队列+生产消费者模型)

JavaWeb 基础知识 --多线程(阻塞队列+生产消费者模型)