Java并发编程:阻塞队列(一)ArrayBlockingQueue
我们都知道ArrayList和LinkedLsit的区别,其实LinkedBlockingQueue和ArrayBlockingQueue之间也存在着类似的区别。因为它们都显现接口BlockingQueue并继承自Queue接口,所以LinkedBlockingQueue和ArrayBlockingQueue的API几乎是一样的,但它们的内部实现原理不太相同,当然使用LinkedBlockingQueue同样也能实现生产者消费者模式。
注:jdk1.7
目录
一、构造器
同样LinkedBlockingQueue的构造器有三个重载版本的构造器有三个重载版本:
public LinkedBlockingDeque() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingDeque(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
}
//创建大小默认值为Integer.MAX_VALUE的阻塞队列并添加c中的元素到阻塞队列
public LinkedBlockingDeque(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock lock = this.lock;
lock.lock(); // Never contended, but necessary for visibility
try {
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (!linkLast(new Node<E>(e)))
throw new IllegalStateException("Deque full");
}
} finally {
lock.unlock();
}
}
LinkedBlockingQueue是一个由链表实现的有界队列阻塞队列,但大小默认值为Integer.MAX_VALUE,所以我们在使用LinkedBlockingQueue时建议手动传值,为其提供我们所需的大小,避免队列过大造成机器负载或者内存爆满等情况。
二、内部成员变量
LinkedBlockingQueue是一个基于链表的阻塞队列,其内部维持一个基于链表的数据队列。
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
//节点类,用于存储数据
static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(E x) { item = x; }
}
// 阻塞队列的大小,默认为Integer.MAX_VALUE
private final int capacity;
// 当前阻塞队列中的元素个数
private transient int count;
// 阻塞队列的头结点
transient Node<E> first;
// 阻塞队列的尾节点
transient Node<E> last;
//获取并移除元素时或增加使用的锁
final ReentrantLock lock = new ReentrantLock();
// notEmpty条件对象,当队列没有数据时用于挂起执行删除的线程
private final Condition notEmpty = lock.newCondition();
// notFull条件对象,当队列数据已满时用于挂起执行添加的线程
private final Condition notFull = lock.newCondition();
}
动态链表嘛,每个添加到LinkedBlockingQueue队列中的数据都将被封装成Node节点,节点中包含下一个next元素和上一个prev元素的信息,其中first和last分别指向队列的头结点和尾结点。
与ArrayBlockingQueue相同,LinkedBlockingQueue中有一把ReentrantLock锁;并且使用了不同的Condition条件对象作为等待队列,用于挂起take线程和put线程。
三、方法显示原理
1、添加方法
对于添加方法,主要指的是add,offer以及put
1)add
我们先来看一下add方法
public boolean add(E e) {
addLast(e);
return true;
}
因为LinkedBlockingQueue底层是一个双向队列,所以在LinkedBlockingQueue中,把add方法细分为了addFirst和addLast分别向队头和队尾插入数据。add方法默认调用的是addLast方法,即默认向队尾添加元素。
public void addLast(E e) {
if (!offerLast(e))
throw new IllegalStateException("Deque full");
}
该方法中调用了offerLast方法,如果返回为true时,会抛出IllegalStateException,即队列已满
public boolean offerLast(E e) {
//添加元素为null直接抛出异常
if (e == null) throw new NullPointerException();
//构建节点
Node<E> node = new Node<E>(e);
//获取锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
return linkLast(node);
} finally {
lock.unlock();
}
}
可见offerLast方法返回的是LinkLast方法
private boolean linkLast(Node<E> node) {
//再次判断队列是否已满,考虑并发情况
if (count >= capacity)
return false;
//获取队尾元素
Node<E> l = last;
//将插入元素的前一个元素指向原队尾元素
node.prev = l;
//插入元素后成为队尾元素
last = node;
//如果队列为空,插入1个元素后,队尾队头指向该元素
if (first == null)
first = node;
else
//队列非空,原队尾元素的下一个元素指向该插入元素
l.next = node;
++count;
//唤醒调用移除方法的线程,执行元素获取操作
notEmpty.signal();
return true;
}
这逻辑其实就和LinkedArray的add方法类似,不过是添加完后会唤醒获取并删除元素的线程
2)offer
public boolean offer(E e) {
return offerLast(e);
}
调用的同是offerLast方法,与addLast不同的是offer方法在队列满时并不会抛出异常,而添加元素的这个操作会进入notEmpty中等待直至队列有空位置,这就需要使用put方法了
3)put
public void put(E e) throws InterruptedException {
putLast(e);
}
public void putLast(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
Node<E> node = new Node<E>(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
while (!linkLast(node))
notFull.await();
} finally {
lock.unlock();
}
}
可见在putLast中调用了linkLast方法循环判断队列是否满了,队满则当前线程将会被notFull条件对象挂起(调用await方法)加到等待队列中,直到队列有空档才会唤醒执行添加操作。
2、移除方法
移除的方法主要是指remove和poll以及take方法
remove同样分为removeFirst和removeLast
public E remove() {
return removeFirst();
}
默认调用removeFirst从队头移除元素并获取该元素
public E removeFirst() {
E x = pollFirst();
if (x == null) throw new NoSuchElementException();
return x;
}
public E pollFirst() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return unlinkFirst();
} finally {
lock.unlock();
}
}
private E unlinkFirst() {
// assert lock.isHeldByCurrentThread();
Node<E> f = first;
if (f == null)
return null;
Node<E> n = f.next;
E item = f.item;
f.item = null;
f.next = f; // help GC
first = n;
if (n == null)
last = null;
else
n.prev = null;
--count;
notFull.signal();
return item;
}
poll方法
public E poll() {
return pollFirst();
}
public E pollFirst() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return unlinkFirst();
} finally {
lock.unlock();
}
}
take方法
public E take() throws InterruptedException {
return takeFirst();
}
public E takeFirst() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E x;
while ( (x = unlinkFirst()) == null)
notEmpty.await();
return x;
} finally {
lock.unlock();
}
}
可见,移除方法的逻辑和对应的添加方法十分类似,不过是队列中节点的两个"指针"移动规则不同,阻塞时由两个不同对象(notFull¬Empty)来存储管理罢了。可以参照添加方法,就不在此赘述了~
看到这里,是不是发现LinkedBlockingQueue除了队列大小有所不同,数据存储容器不同(导致实现原理不同)与ArrayBlockingQueue在线程阻塞方面十分类似呢。不过是在不能满足添加或删除条件时放到两个Condition中,当满足了就分别取唤醒阻塞中的线程。这篇博客就当复习LinkedList和加深阻塞队列印象了:)