在学习完java的同步队列、Lock和等待通知机制之后,再来看阻塞队列会觉得阻塞队列更加容易理解。阻塞队列是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入:当队列满时,队列会阻塞插入元素的线程,直到队列不满;阻塞移除:在队列为空时,获取元素的线程会等待队列变为非空。在阻塞队列不可用时,这两个附加操作提供了4种处理方式
方法/处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
插入方法 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
移除方法 | remove() | poll() | take() | poll(time,unit) |
检查方法 | element() | peek() | - | - |
·抛出异常是指当队列满时,如果再往队列里插入元素,会抛出IllegalStateException("Queue full")异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常。
返回特殊值是指当往队列插入元素时,会返回元素是否插入成功,成功返回true。如果是移 除方法,则是从队列里取出一个元素,如果没有则返回null。
·一直阻塞是指当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者 线程,直到队列可用或者响应中断退出。当队列空时,如果消费者线程从队列里take元素,队 列会阻塞住消费者线程,直到队列不为空。后面我们会介绍这种模式的源码。
·超时退出是指当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程 一段时间,如果超过了指定的时间,生产者线程就会退出。
jdk为我们提供了一下几种阻塞队列,他们的实现方式几乎是相同的,我们后面以一种方式讲述阻塞队列的实现,如下表为jdk为我们提供的几种阻塞队列与描述:
阻塞队列 | 描述 |
ArrayBlockingQueue | 一个由数组结构组成的有界阻塞队列 |
LinkedBlockingQueue | 一个由链表结构组成的有界阻塞队列 |
PriorityBlockingQueue | 一个支持优先级排序的无界阻塞队列 |
DelayQueue | 一个使用优先级队列实现的无界阻塞队列 |
SynchronousQueue | 一个不存储元素的阻塞队列 |
LinkedTransferQueue | 一个由链表结构组成的无界阻塞队列 |
LinkedBlockingDeque | 一个由链表结构组成的双向阻塞队列 |
如上,为jdk为我们提供的阻塞队列,如果我们查看它们的源码,发现它们的变量定义中都有Lock接口和Condition接口作为字段,也就是说,他们的实现是以Lock和Condition作为基础的。只要我们掌握了Lock和Condition的使用,就很容易掌握阻塞队列的使用。我们以LinkedBlockingQueue为例,定义代码如下:
/** Lock held by take, poll, etc */ take、poll操作时获取锁
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */等待take操作
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */ put offer时获取锁
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */ //等待put操作
private final Condition notFull = putLock.newCondition();
接下来我们要看,LinkedBlockingQueue是如何存储元素和取出元素的,首先我们以存储元素为例,我们以put方法为例,代码如下所示:
//唤醒一个take等操作的线程
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
public void put(E e) throws InterruptedException {
//如果传值为null,抛出异常
if (e == null) throw new NullPointerException();
int c = -1;
//创建节点
Node<E> node = new Node<E>(e);
//获取putLock实例
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
//获取锁,可中断
putLock.lockInterruptibly();
try {
//如果容量已满
while (count.get() == capacity) {
//存放元素的线程等待
notFull.await();
}
//存入队列
enqueue(node);
c = count.getAndIncrement();
//如果队列不满,唤醒存入队列的线程
if (c + 1 < capacity)
notFull.signal();
} finally {
//释放锁
putLock.unlock();
}
//如果为0
if (c == 0)
//唤醒一个取出元素的线程
signalNotEmpty();
}
上面我们介绍了存放元素的源码,其他几种存放元素的逻辑与上面的类似,这里不作介绍,下面我们分析获取元素的源码:take方法,源码如下:
//唤醒一个put操作的线程
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
//获取take锁
takeLock.lockInterruptibly();
try {
//如果队列元素为空,获取线程等待
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
//如果队列元素不为空,唤醒取元素线程
if (c > 1)
notEmpty.signal();
} finally {
//释放锁
takeLock.unlock();
}
//如果c=capacity 唤醒put操作的线程
if (c == capacity)
signalNotFull();
return x;
}
上面我们介绍了LinkedBlockingQueue的put和take操作,可以知道阻塞队列的主要原理就是等待通知机制,使用了并发发包提供的Lock、Condition、LockSupport组件。只要我们熟悉了这三个组件的的使用就能容易实现自己的阻塞队列。如果想要知道其他几种阻塞队列的具体实现可以自行参阅源码,其核心就是等待通知机制以及上面所说的三个组件。