1.简介
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。
1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。
2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。
阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。
在阻塞队列不可用时,这插入、移除这两个附加操作提供4种处理方式:
方法/处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add(E) | offer(E) | put(E) | offer(E,long,TimeUnit) |
移除方法 | remove() | poll() | take() | poll(long,TimeUnit) |
查看方法 | element() | peek() | 无 | 无 |
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();
}
}
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类似,只是这里是个限时版本。