前言:
上一部分,我们一起学习了ConcurrentHashMap的概念,部分实现原理及使用,这一部分,我们来学习java中的阻塞队列
1. 什么是阻塞队列
阻塞队列是一个支持阻塞插入和阻塞移除方法的队列。支持阻塞的插入方法的意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。支持阻塞的移除的方法意思是当队列为空时,获取元素的线程会等待队列变为非空。阻塞队列一般用于生产者消费者场景,生产者向队列里添加元素,消费者从队列里取元素。阻塞队列就是存放和获取元素的容器。
在阻塞队列不可用时,插入和删除元素提供了4种处理方式,如下表所示
处理方式 | 抛出异常 | 返回值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
删除 | remove() | poll() | take() | poll(time,unit) |
检查 | element() | peek() |
抛出异常:队列满时,再往队列中插入元素,会抛出IllegalStateException异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常。
返回特殊值:往队列插入元素时,会判断插入是否成功,成功则返回true。如果是移除元素,则从队列取出一个元素,如果没有则返回null。
一直阻塞:当队列满时,如果往队列里插入元素,队列会一直阻塞直到队列可用或响应中断。当队列空时,如果从队列中取出元素,队列会阻塞,直到队列空为止。
超时退出:当队列满时,如果往队列插入元素,队列会阻塞一段指定的时间,如果超过指定时间,阻塞线程就会退出。
2. java中的阻塞队列
- ArrayBlockingQueue:一个由数组组成的有界阻塞队列
- LinkedBlockingQueue:一个由链表组成的有界阻塞队列
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列
- DelayQueue:一个使用优先级队列实现的无界阻塞队列
- SynchronousQueue:一个不存储元素的阻塞队列
- LinkedTransferQueue:一个由链表组成的无界阻塞队列
- LinkedBlockingQueue:一个由链表组成的双向阻塞队列
ArrayBlockingQueue
此队列按照先进先出(FIFO)规则对元素排序,默认情况下不保证线程公平的访问队列,公平访问队列的含义时阻塞的线程可以按照阻塞的先后顺序访问线程也就是先阻塞的先访问,而非公平的访问意思是当队列可用时,阻塞的线程都可以争夺访问队列的资格。
LinkedBlockingQueue
此队列默认和最大的长度都是Integer.MAX_VALUE,队列按照先进先出原则对元素排序
PriorityBlockingQueue
默认情况下该队列采用自然顺序升序排列,也可以自定义实现compareTo()方法来指定元素排序规则。
DelayQueue
队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才可以从队列中获取元素
SynchronousQueue
是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。默认采用非公平访问策略访问队列,但是也支持公平访问队列。SynchronousQueue可以看成传球手,队列本身并不存储任何元素,非常适合传递性场景。
LinkedTransferQueue
LinkedTransferQueue采用一种预占模式。意思就是消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,那就生成一个节点(节点元素为null)入队,然后消费者线程被等待在这个节点上,后面生产者线程入队时发现有一个元素为null的节点,生产者线程就不入队了,直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素,从调用的方法返回。我们称这种节点操作为“匹配”方式。
LinkedBlockingQueue
是一个双向阻塞队列。双向队列指的就是可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。
3. 阻塞队列的实现原理
如果队列是空的,消费者会一直等待,当生产者添加元素时,消费者是如何知道当前队列有元素的呢?我们以ArrayBlockingQueue为例来看看JDK源码是怎么实现的。
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
insert(e);
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return extract();
} finally {
lock.unlock();
}
}
private void insert(E x) {
items[putIndex] = x;
putIndex = inc(putIndex);
++count;
notEmpty.signal();
}
当往队列里插入一个元素时,如果队列不可用,那么阻塞生产者主要通过LockSupport.park(this)来实现。
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
总结
这一部分,我们一起学习了java中的阻塞队列的基本概念和基本方法,下一部分,我们将学习Fork/Join框架。