并发编程中的阻塞队列概述

1.简介

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。 1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。 2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。 阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。

在阻塞队列不可用时,插入、移除这两个附加操作提供4种处理方式:

4种处理试的说明:

抛出异常:当队列满时,如果再往队列里插入元素,会抛出IllegalStateException("Queue full")异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常。

返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。如 果是移除方法,则是从队列里取出一个元素,如果没有则返回null。

一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者线程从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。

超时退出:当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超过了指定的时间,生产者线程就会退出。

注意:如果有无界阻塞队列,队列永不会满,put或offer方法永不会被阻塞,使用offer方法永远返回true.

2.阻塞队列分类

主要的几种阻塞队列

1)普通阻塞队列:ArrayBlockingQueue,基于数组的有界阻塞队列 ;LinkedBlockingQueue ,基于单向链表的有界阻塞队列;LinkedBlockingDeque, 基于双向链表的有界双向阻塞队列。

2)优先级阻塞队列:PriorityBlockingQueue, 基于优先级排序无界阻塞队列。

3)延时阻塞队列:DelayQueue,基于优先级队列实现的无界阻塞队列。

4)其他阻塞队列:SynchronousQueu,不存储元素的阻塞队列;LinkedTransferQueue,基于单向链表组成的无界阻塞队列。

阻塞队列实现的基本原理:

阻塞队列主要是利用并发编程中的“等待/通知”模型,使用显式锁ReentrantLock和条件ConditionObject实现的。如下自定义的阻塞队列BoundBlockQueue,它使用一把锁和两个条件来实现阻塞队列。一个把锁lock,可以保护所有的访问,一个条件notEmpty用来通知线程当前队列“不满”,另一个条件notFull用来通知线程当前队列“不空”。上面介绍的7种阻塞队列其实现原理大致与此类似。

package thread;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class BoundBlockQueue<T> {
    private       int       addIndex    = 0;
    private       int       removeIndex = 0;
    private       int       count       = 0;
    private final Object[]  items;
    private final Lock      lock        = new ReentrantLock();
    private final Condition notEmpty    = lock.newCondition();
    private final Condition notFull     = lock.newCondition();

    public BoundBlockQueue(int size) {
        if (size < 0) throw new IllegalArgumentException("size must large than zero");
        this.items = new Object[size];
    }

    public void add(T o) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length) notFull.await();
            items[addIndex++] = o;
            count++;
            if (addIndex == items.length) addIndex = 0;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public T remove() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) notEmpty.await();
            Object o = items[removeIndex];
            items[removeIndex++] = null;
            count--;
            if (removeIndex == items.length) removeIndex = 0;
            notFull.signal();
            return (T) o;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        BoundBlockQueue<String> queue = new BoundBlockQueue<>(9);
/*        new Thread(() -> {
            try {
                int i = 0;
                while(i<=5){
                    queue.add(String.valueOf(i));
                    System.out.println("add-digital: " + i);
                    Thread.sleep(1000);
                    i++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();*/
        new Thread(() -> {
            try {
                int i = 1;
                while (i <= 10) {
                    queue.add("0" + i);
                    System.out.println("add-digital: 0" + i);
                    Thread.sleep(500);
                    i++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "addItemThread1").start();
        new Thread(() -> {
            try {
                int i = 0;
                while (i <= 8) {
                    String s = queue.remove();
                    System.out.println("remove-digital:" + s);
                    Thread.sleep(1000);
                    i++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "removeItemThread1").start();
    }
}
BoundBlockQueue

3.阻塞队列详述

1)普通阻塞队列

ArrayBlockingQueue和LinkedBlockingQueue都实现了Queue接口,表示先进先出的队列,头出尾进,而LinkedBlockingDeque实现了Deque接口,它表示一个双端队列,头尾均可进出。

这三个阻塞队列内部都是使用显式锁ReentrantLock和显式条件CoditionObject实现的。

(1)ArrayBlockingQueue

ArrayBlockingQueue是一个用数组实现的有界阻塞队列(构造方法必须指定容量) ,创建时指定容量大小,且运行时容量也不会变化。默认情况下不保证线程公平地访问队列。此队列按照先进先出(FIFO)的原则对元素进行排序。队列的容量和公平性选择均在构造方法中设定。

如下便创建了一个容量为50的公平阻塞队列。

ArrayBlockingQueue e = new ArrayBlockingQueue(50,true);

ArrayBlockingQueue的实现比较简单,其内部有一个数组存储元素,有两个索引分别表示头和尾,有一个变量表示当前元素个数,有一个锁保护所有的访问,有“不满‘和”不空“两个条件处理线程协作问题。

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
    private static final long serialVersionUID = -817911632652898426L;
    final Object[] items;
    int takeIndex;
    int putIndex;
    int count;
    final ReentrantLock lock;
    private final Condition notEmpty;
    private final Condition notFull;
    //.....省略
}

(2)LinkedBlockingQueue

LinkedBlockingQueue是基于单向链表实现的,在创建是可指定最大长度,也可不指定,默认是无限大。LinkedBlockingQueue不支锁的公平性选择,只支持非公平锁。

LinkedBlockingQueue queue=new LinkedBlockingQueue();
LinkedBlockingQueue queue2=new LinkedBlockingQueue(20);

LinkedBlockingQueue的实现与ArrayBlockingQueue有些不同,它是单向链表结构,尾进头出,为提高性能它将锁的粒度细化了,使用了两个锁,一个保护头部、一个保护尾部,每个锁绑定一个条件。

public class LinkedBlockingQueue<E> extends AbstractQueue<E>
            implements BlockingQueue<E>, java.io.Serializable {
        private final int capacity;
        private final AtomicInteger count = new AtomicInteger();
        transient Node<E> head;
        private transient Node<E> last;
        private final ReentrantLock takeLock = new ReentrantLock();//保护头部(出队)的锁
        private final Condition notEmpty = takeLock.newCondition();//"不空"条件
        private final ReentrantLock putLock = new ReentrantLock();//保护尾部(入队)的锁
        private final Condition notFull = putLock.newCondition();//“不满”条件
    //......省略

(3)LinkedBlockingDeque

LinkedBlockingDeque,基于双向链表实现,其最大长度可也是可选的,默认无限大。LinkedBlockingDeque不支锁的公平性选择,只支持非公平锁。

LinkedBlockingDeque deque = new LinkedBlockingDeque();
LinkedBlockingDeque deque2 = new LinkedBlockingDeque(20);

LinkedBlockingDeque,与ArrayBlockingQueue类似,也是使用一个锁,两个条件,使用锁保护所有操作,使用“不满‘和”不空“两个条件进行线程协作。

public class LinkedBlockingDeque<E> extends AbstractQueue<E>
        implements BlockingDeque<E>, java.io.Serializable {
    transient Node<E> first;
    transient Node<E> last;
    private transient int count;
    private final int capacity;
    final ReentrantLock lock = new ReentrantLock(); //重入锁
    private final Condition notEmpty = lock.newCondition();//"非空"条件
    private final Condition notFull = lock.newCondition();//“非满”条件
    //......省略
}

LinkedBlockingDeque是双向队列,它可以在队列的两端插入和移出元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。因其有两个插入/移除的入口,双向阻塞队列常用在Jork/Join框架的“工作窃取”模式中。

2)优先级阻塞队列

ProrityBlockingQueue是一个支持优先级的无界阻塞队列,(内部元素排列不是完全有序)它是按照优先级出队的(不像普通阻塞队列那样先进先出)。其内部使用数组来存储元素(逻辑上是堆,物理上是数组),它是无界的,数组的长度会动态扩展。它要求元素类型实现了Comparable接口或在构造方法中传入一个Comparator比较器,另外它不能保证同优先级元素的顺序。

public class PriorityBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
    private static final long serialVersionUID = 5595510919245408276L;
    private transient Object[] queue; //保存元素的数组
    private transient int size;
    private transient Comparator<? super E> comparator;
    private final ReentrantLock lock; //
    private final Condition notEmpty;//“不空”的条件
    private transient volatile int allocationSpinLock;
    private PriorityQueue<E> q;//只有序列化/反序列化会用到,主要目的是与以前版本兼容。
    //......省略
}

ProrityBlockingQueue不支锁的公平性选择,只支持非公平锁。ProrityBlockingQueue的实现,它有一个数组保存元素,它使用一把锁保护所有的操作,(因它是无界的,永不会“满”)只使用一个“不空”的条件进行线程协作。

3)延时阻塞队列

DelayQueue是一个支持延时获取元素的无界阻塞队列。它内部使用一个优先级队列来储存元素,使用一把锁来保护数据,用一个“可获取”的条件表示头部是否有元素可获取,当头部元素的延时未到时,take操作会根据延时计算需要休眠的时间,然后休眠,若此进程中有新的元素入队,且成为头部元素,则部位皮肤休眠的线程会被提前唤醒然后重新检查延时。DelayQueue不支锁的公平性选择,只支持非公平锁。

public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
        implements BlockingQueue<E> {

    private final transient ReentrantLock lock = new ReentrantLock();
    private final PriorityQueue<E> q = new PriorityQueue<E>();
    private Thread leader = null;
    private final Condition available = lock.newCondition();
    //.....省略
}

它要求元素类型要实现Delay接口,而Delay接口双继承Comparable接口,那么DelayQueue中的每一个元素都是可比较的,额外的getDelay方法返回一个再延迟多长时间的整数(小于等于零就不再延迟)。

public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit unit);
}

DelayQueue常用于定时任务,因为DelayQueue是按照延时时间出队的。元素只有在过期后才能出队列中拿走,若没过期就要阻塞等待。

4)其他阻塞队列

SynchronousQueue和LinkedTransferQueue是两个特殊的阻塞队列。

(1)SynchronousQueue

SynchronousQueue,与其他队列不同,它没有存储元素的空间,它的每入队操作必须等待另一个线程的出队操作,反之亦然。

它支持公平访问队列(通过构造方法的参数指定),默认情况下线程采用非公平性策略访问队列

SynchronousQueue适用于两个线程之间直接传递信息、事件或任务。SynchronousQueue的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue。

(2)LinkedTransferQueue

LinkedTransferQueue实现了TransferQueue接口,TransferQueue扩展了BlockingQueue接口,增加了一此其他功能.生产者在往队列中放元素时,可以等待消费者接收后再返回,适用于消息传递类型的应用。

public interface TransferQueue<E> extends BlockingQueue<E> {
    //如果有消费者在等待,直接转给消费者,返回true,否则返回false,不入队
    boolean tryTransfer(E e);
    //如果有消费者在等待,直接转给消费者,否则入队,阻塞等待直到被消费者接口后再返回
    void transfer(E e) throws InterruptedException;
    //如果有消费者在等待,直接转给消费者,返回true,否则入队,阻塞等待限定时间,若最后被消费者接收,返回true
    boolean tryTransfer(E e, long timeout, TimeUnit unit) throws InterruptedException;
    //是否有消费者在等待
    boolean hasWaitingConsumer();
    //在等待的消费者个数
    int getWaitingConsumerCount();
}

LinkedTransferQueue,与其他阻塞队列相比,主要是多了transfer系列方法, 且这几个方法核心逻辑都是委托给xfer方法实现的。

public void transfer(E e) throws InterruptedException {
        if (xfer(e, true, SYNC, 0) != null) {
            Thread.interrupted(); // failure possible only due to interrupt
            throw new InterruptedException();
        }
    }
   public boolean tryTransfer(E e, long timeout, TimeUnit unit)
            throws InterruptedException {
        if (xfer(e, true, TIMED, unit.toNanos(timeout)) == null)
            return true;
        if (!Thread.interrupted())
            return false;
        throw new InterruptedException();
    }
    public boolean tryTransfer(E e) {
        return xfer(e, true, NOW, 0) == null;
    }

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

tryTransfer(E)方法 : 与transfer类似,只这里只是“尝试”,若没有消费者在等待,它不会阻塞,会直接返回。

transfer(E,long,TimeUnit)方法: 与transfer类似,只是这里是个限时版本。

 

posted @ 2020-03-08 17:01  蜀中孤鹰  阅读(489)  评论(0编辑  收藏  举报