前置知识点:ReentrantLock与AQS的核心实现,链接。
BlockingQueue
首先看它们共同的接口BlockingQueue
java
public interface BlockingQueue<E> extends Queue<E> {
boolean add(E e);
boolean offer(E e);
void put(E e) throws InterruptedException;
boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException;
E take() throws InterruptedException;
E poll(long timeout, TimeUnit unit)
throws InterruptedException;
E element();
// 获取但不移除此队列的头元素,如果此队列为空,就返回null
E peek();
int remainingCapacity();
boolean remove(Object o);
public boolean contains(Object o);
int drainTo(Collection<? super E> c);
int drainTo(Collection<? super E> c, int maxElements);
}
主要需要了解的是add、offer、put等添加元素的方法以及take和poll这两个删除元素的方法。、
其中
- add、offer在实现上一般是非阻塞式的添加,也就是获取会直接返回失败或者成功。
- put方法是实现阻塞式添加元素,队列满会在对应的条件变量上进行等待。
- poll是非阻塞式的获取元素,也就是获取会直接返回失败或者成功。
- take是实现阻塞式的获取元素,队列空会在队列的条件队列上进行等待。
ArrayBlockingQueue和LinkeBlockingQueue的大致区别
- ArrayBlockingQueue 使用定长数组存储元素,添加和删除元素使用的是同一把锁。内部保存了两个变量标识队列头部和尾部在数组中的位置,并且额外用一个变量count记录数组中的元素个数,元素个数用来方便获取队列的空和满两种状态。
- LinkeBlockingQueue 使用链表存储,添加和删除元素使用的不同的锁,实现添加和删除元素的锁分离,意味着LinkeBlockingQueue 在高并发场景下生产者和消费者可以并行地操作队列中的数据,事实上ArrayBlockingQueue也可以实现添加和删除的锁的分离操作使添加和删除的操作并行。
- LinkeBlockingQueue 在添加和删除元素是会生成一个额外的Node 实例,因为使用的是链表。那么在长时间、高并发的大数据量场景下LinkeBlockingQueue 会产生额外的Node 实例增大GC的压力。
- ArrayBlockingQueue 是强制指定初始容量使用,而LinkeBlockingQueue 在指定容量的时候和ArrayBlockingQueue 表现相同,否则LinkeBlockingQueue 的容量为Integer.MAX_VALUE,这样会成为一个无界队列,在生产者生产速度过快的情况下会造成内存溢出。
ArrayBlockingQueue
ArrayBlockingQueue的构造器和成员介绍
整体设计
通过继承AbstractQueue和实现BlockingQueue从而拥有集合的常见操作方法和特性以及阻塞队列的方法然后自己去实现对应的功能。
ArrayBlockingQueue是可以选择创建公平队列和非公平队列的。
- 公平队列,被阻塞的线程按照阻塞的先后顺序访问队列。
- 非公平队列,如果队列可用,阻塞的线程将会争夺资源的访问权,没有固定执行顺序。
- 这里的公平性和非公平性是通过ReentrantLock 的公平锁和非公平锁实现的。默认是非公平MODE。
1. ArrayBlockingQueue构造器
csharp
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
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 ArrayBlockingQueue(int capacity, boolean fair,
Collection<? extends E> c) {
this(capacity, fair);
final ReentrantLock lock = this.lock;
lock.lock(); // Lock only for visibility, not mutual exclusion
try {
int i = 0;
try {
for (E e : c) {
checkNotNull(e);
items[i++] = e;
}
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
2. ArrayBlockingQueue的成员介绍
arduino
private static final long serialVersionUID = -817911632652898426L;
/** The queued items */
final Object[] items;
/** items index for next take, poll, peek or remove 下一个取出元素的位置*/
int takeIndex;
/** items index for next put, offer, or add 下一个添加元素的位置*/
int putIndex;
/** Number of elements in the queue 数组中的元素个数*/
int count;
/*
* Concurrency control uses the classic two-condition algorithm
* found in any textbook.
*/
/** Main lock guarding all access 读写操作都需要获取这个锁,读写锁不分离 */
final ReentrantLock lock;
/** Condition for waiting takes 阻塞获取的条件变量*/
private final Condition notEmpty;
/** Condition for waiting puts 阻塞添加的条件变量*/
private final Condition notFull;
-
可能会有人对notEmpty和notFull这两个条件变量的操作存在疑问。
-
两个方法signal和await
- signal方法就是表示变量名的操作,也就是说notEmpty.signal()是表示队列非空,需要唤醒。
- await方法就是表示变量名的相反操作,也就是说notEmpty.await()是表示队列为空,需要等待。
这里介绍一下两个index
- 这里需要知道ArrayBlockingQueue是通过取模的操作实现循环数组的,当队列不满的时候说明putIndex和takeIndex之间有位置可以插入元素。
添加元素解析
非阻塞式add()、offer()解析
这两个方法是用来实现非阻塞式的添加元素的。
typescript
// ArrayBlockingQueue继承了AbstractQueue
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable
public boolean add(E e) {
return super.add(e);
}
// 实际上add方法是ArrayBlockingQueue的父类AbastractQueue的add方法, add方法的实现如下, 最终也是调用了offer方法
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
// offer方法拿到锁之后判断队列会否满
// 1. 数组满直接释放锁,返回false,添加失败
// 2. 数组没满,将元素加入数组,释放锁,返回true,添加成功
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
// 能执行插入操作是已经获取到锁了
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
最终添加数组元素是enqueue(E e)方法进行入队
ini
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
// 如果当前位置走到头了,就从0开始,实现一个循环的效果,是否能插入由count和length去控制
if (++putIndex == items.length)
putIndex = 0;
count++;
// 有元素插入唤醒在空条件上等待的消费线程
notEmpty.signal();
}
阻塞式的put()方法
csharp
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();
}
}
- 可以被中断。
- 队列满的时候在notFull条件队列上进行等待。
- 队列元素未满通过enqueue方法添加元素。
删除元素
非阻塞式的删除poll()
csharp
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
ini
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
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;
}
- 进入dequeue表示takeIndex的元素可以删除,如果不能就不会进入此方法。
阻塞式删除
csharp
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
- 这里基本上和阻塞式获取是属于镜像,执行相反操作即可。
- take也是可中断的。
获取元素 peek
csharp
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return itemAt(takeIndex); // null when queue is empty
} finally {
lock.unlock();
}
}
final E itemAt(int i) {
return (E) items[i];
}
LinkeBlockingQueue
LinkeBlockingQueue的构造器与成员介绍
整体设计
它和ArrayBlockingQueue不同之处就在实现上。
1. LinkeBlockingQueue的构造器
csharp
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
putLock.lock(); // Never contended, but necessary for visibility
try {
int n = 0;
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}
- 需要注意的是LinkeBlockingQueue在不指定初始大小的情况下时是一个无界队列(Integer.MAX_VALUE),可能会造成内存溢出。
LinkedBlockingQueue的成员及唤醒函数
csharp
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
private static final long serialVersionUID = -6903933977591709194L;
/**
* Linked list node class 实现队列的链表
*/
static class Node<E> {
E item;
/**
* One of:
* - the real successor Node
* - this Node, meaning the successor is head.next
* - null, meaning there is no successor (this is the last node)
*/
Node<E> next;
Node(E x) { item = x; }
}
/** The capacity bound, or Integer.MAX_VALUE if none 自定义的容量大小,不指定为*/
private final int capacity;
/** Current number of elements 链表中的节点个数,使用原子变量*/
private final AtomicInteger count = new AtomicInteger();
/**
* Head of linked list.
* Invariant: head.item == null
* 链表的头结点
*/
transient Node<E> head;
/**
* Tail of linked list.
* Invariant: last.next == null
* 链表的尾结点
*/
private transient Node<E> last;
/** Lock held by take, poll, etc 读锁*/
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc 写锁*/
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
/**
* Signals a waiting take. Called only from put/offer (which do not
* otherwise ordinarily lock takeLock.)
* 非空唤醒
*/
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
/**
* Signals a waiting put. Called only from take/poll.
* 队列不满唤醒操作
*/
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
/**
* Locks to prevent both puts and takes.
*/
void fullyLock() {
putLock.lock();
takeLock.lock();
}
/**
* Unlocks to allow both puts and takes.
*/
void fullyUnlock() {
takeLock.unlock();
putLock.unlock();
}
- fullyLock和fullyUnlock使用在移除元素以及判断元素是否存在于队列中这些操作的地方,相当于和ArrayBlockingQueue的一个锁实现一样的功能。
添加元素解析
非阻塞式添加offer
ini
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
if (e == null) throw new NullPointerException();
long nanos = unit.toNanos(timeout);
int c = -1;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos);
}
enqueue(new Node<E>(e));
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return true;
}
// 添加元素的合法性判断
// 1. 获取队列元素看是否队列可用,不可用返回false
// 2. 如果可用就获取写锁putLock, 然后再检查队列是否可用,可用才进行入队操作
// 3. 在添加元素的时候还进行的notFull条件队列的唤醒操作,只要队列可用就可以唤醒生产线程生产
// 4. 最后判断元素是否添加成功,如果是从
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity)
return false;
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
if (count.get() < capacity) {
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
}
} finally {
putLock.unlock();
}
// 这里比较迷惑,是因为上面获取是通过count.getAndIncrement();方法得到值
// 如果c == 0的话说明插入之前队列为空,这时需要去唤醒消费线程。
if (c == 0)
signalNotEmpty();
return c >= 0;
}
// enqueue操作非常简单,转换链表结点的指针插入结点即可
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}
阻塞式添加put()
ini
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node<E> node = new Node<E>(e);
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();
}
if (c == 0)
signalNotEmpty();
}
- put方法的操作基本和offer方法相同,不同的是如果队列不可用会进行等待而不是返回false。
- put也是可中断的。
- 与ArrayBlockingQueue不同的地方是每次插入需要创建Node结点。
获取元素解析
非阻塞式获取poll()
ini
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
E x = null;
int c = -1;
long nanos = unit.toNanos(timeout);
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0)
return null;
E x = null;
int c = -1;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
if (count.get() > 0) {
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
//如果队列不为空,唤醒入队等待线程
if (c == capacity)
signalNotFull();
return x;
}
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // 帮助 GC
head = first;
E x = first.item;
first.item = null;
return x;
}
// 返回队列首元素,不会出队
public E peek() {
if (count.get() == 0)
return null;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
Node<E> first = head.next;
if (first == null)
return null;
else
return first.item;
} finally {
takeLock.unlock();
}
}
阻塞式获取take
ini
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
- take可中断。
总结
LinkeBlockingQueue 和ArrayBlockingQueue的核心源码大致就是以上的实现。
LinkedBlockingQueue 和ArrayBlockingQueue的不同点在于:
- 队列大小不同,后者 是有界的初始化需必须指定大小,而前者可以是有界的也可以是无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。
- 数据存储容器不同,后者 采用的是定长数组作为数据存储容器,而前者 采用的则是以Node节点作为连接对象的链表。
- 由于后者 采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而前者 则会生成一个额外的Node 对象,所以前者在出队的时候需要help GC。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响。
- 两者的实现队列添加或移除的锁不一样,后者 实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,而前者 实现的队列中的锁是分离的,添加采用putLock ,移除采用takeLock,这样能提高队列的吞吐量,在高并发的情况下生产者和消费者可以并行地操作队列中的数据,从而提高整个队列的并发性能。
- 最后,ArrayBlockingQueue也不是不能实现读锁和写锁的分离, 那为什么不这样做呢?可能是因为
- LinkedBlockingQueue 是由链表组成操作的分别是头尾节点,相互竞争的关系较小。而 ArrayBlockingQueue 是数组,添加和删除都是在同一个数组上,读写锁的分离事实上也可以增加并发性能,但是可能由于实现起来更加复杂而放弃使用读写锁分离。
- LinkedBlockingQueue 添加元素时有一个构造节点的时间(写的时候耗时较高,影响读),为了尽量减少这部分时间占比,使用一个读锁一个写锁可以实现并发存取的优化。而 ArrayBlockingQueue 由于添加和删除都不涉及对象的创建就没有用读写锁分离。
参考文献
《Java高并发核心编程 卷2(加强版):多线程、锁、JMM、JUC、高并发设计模式》 尼恩