什么是阻塞队列?
一个支持两个附加操作的队列。这两个附加操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者场景。
非阻塞队列与阻塞队列处理方法对比:
非阻塞队列中几个主要方法有:
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客户端数据读取和解析。读取数据的线程不断将数据放入队列。解析线程不断从队列取数据解析。
Demo:利用BlockingQueue实现生产者、消费者模型
/** * @Title: Resource * @Description: 资源池 * @date 2019/1/1810:31 */ public class Resource { private static final Logger logger = LoggerFactory.getLogger(com.imooc.demo.thread.Resource.class); private BlockingQueue blockingQueue = new LinkedBlockingDeque(10); /** * 取资源 */ public synchronized void remove(){ try { blockingQueue.take(); logger.info("消费者" + Thread.currentThread().getName() + "消耗一件资源," + "当前资源池有" + blockingQueue.size()+ "个资源"); } catch (InterruptedException e) { logger.error("remove error {}",e); } } /** * 添加资源 */ public synchronized void add(){ try { blockingQueue.put(1); logger.info("生产者" + Thread.currentThread().getName()+ "生产一件资源," + "当前资源池有" + blockingQueue.size() + "个资源"); } catch (InterruptedException e) { logger.error("add error {}",e); } } } /** * @Title: ConsumerThread * @Description: 消费者 * @date 2019/1/1810:07 */ public class ConsumerThread extends Thread{ private static final Logger logger = LoggerFactory.getLogger(ConsumerThread.class); private Resource resource; public ConsumerThread(Resource resource){ this.resource = resource; } public void run(){ while (true){ try { Thread.sleep(3000); } catch (InterruptedException e) { logger.error("ConsumerThread error {}",e); } resource.remove(); } } } /** * @Title: ProducerThread * @Description: 生产者 * @date 2019/1/1810:08 */ public class ProducerThread extends Thread{ private static final Logger logger = LoggerFactory.getLogger(ProducerThread.class); private Resource resource; public ProducerThread(Resource resource){ this.resource = resource; } public void run(){ while (true){ try { Thread.sleep(5000); } catch (InterruptedException e) { logger.error("ProducerThread error {}",e); } resource.add(); } } } /** * @Title: Client * @Description: 客户端 * @date 2019/1/1810:10 */ public class Client { public static void main(String[] args) { Resource resource =new Resource(); ProducerThread pt1 = new ProducerThread(resource); ConsumerThread ct1 = new ConsumerThread(resource); ConsumerThread ct2 = new ConsumerThread(resource); pt1.start(); ct1.start(); ct2.start(); } }