ArrayBlockingQueue是Java并发框架中阻塞队列的最基本的实现,分析这个类就可以知道并发框架中是如何实现阻塞的。它是数组实现的线程安全的有界阻塞队列。线程安全是指,ArrayBlockingQueue内部通过“互斥锁”保护竞争资源,实现了多线程对竞争资源的互斥访问。而有界,则是指ArrayBlockingQueue对应的数组是有界限的。 阻塞队列,是指多线程访问竞争资源时,当竞争资源已被某线程获取时,其它要获取该资源的线程需要阻塞等待,ArrayBlockingQueue实现阻塞队列的关键在与,对锁(Lock)和等待条件(Condition)的使用,这两个实现的基本功能类似域wait()和notify(),是wait()和notify()的高级用法;而且,ArrayBlockingQueue是按 FIFO(先进先出)原则对元素进行排序,元素都是从尾部插入到队列,从头部开始返回。
注意:ArrayBlockingQueue不同于ConcurrentLinkedQueue,ArrayBlockingQueue是数组实现的,并且是有界限的;而ConcurrentLinkedQueue是链表实现的,是无界限的。
1.ArrayBlockingQueue原理和数据结构
ArrayBlockingQueue的数据结构,如下图所示:
public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {} //继承体系 java.lang.Object java.util.AbstractCollection<E> java.util.AbstractQueue<E> java.util.concurrent.ArrayBlockingQueue<E> //实现的所有接口: Serializable, Iterable<E>, Collection<E>, BlockingQueue<E>, Queue<E>
//在前面我们学习与synchronized锁配合的线程等待(Object.wait)与线程通知(Object.notify), //那么对于JDK1.5 的 java.util.concurrent.locks.ReentrantLock 锁,JDK也为我们提供了与此功能相 //应的类java.util.concurrent.locks.Condition。Condition与重入锁是通过lock.newCondition()方 //法产生一个与当前重入锁绑定的Condtion实例,我们通知该实例来控制线程的等待与通知 public interface Condition { //使当前线程加入 await() 等待队列中,并释放当锁,当其他线程调用signal()会重新请求锁。与Object.wait()类似。 void await() throws InterruptedException; //调用该方法的前提是,当前线程已经成功获得与该条件对象绑定的重入锁,否则调用该方法时会抛出IllegalMonitorStateException。 //调用该方法后,结束等待的唯一方法是其它线程调用该条件对象的signal()或signalALL()方法。等待过程中如果当前线程被中断,该方法仍然会继续等待,同时保留该线程的中断状态。 void awaitUninterruptibly(); // 调用该方法的前提是,当前线程已经成功获得与该条件对象绑定的重入锁,否则调用该方法时会抛出IllegalMonitorStateException。 //nanosTimeout指定该方法等待信号的的最大时间(单位为纳秒)。若指定时间内收到signal()或signalALL()则返回nanosTimeout减去已经等待的时间; //若指定时间内有其它线程中断该线程,则抛出InterruptedException并清除当前线程的打断状态;若指定时间内未收到通知,则返回0或负数。 long awaitNanos(long nanosTimeout) throws InterruptedException; //与await()基本一致,唯一不同点在于,指定时间之内没有收到signal()或signalALL()信号或者线程中断时该方法会返回false;其它情况返回true。 boolean await(long time, TimeUnit unit) throws InterruptedException; //适用条件与行为与awaitNanos(long nanosTimeout)完全一样,唯一不同点在于它不是等待指定时间,而是等待由参数指定的某一时刻。 boolean awaitUntil(Date deadline) throws InterruptedException; //唤醒一个在 await()等待队列中的线程。与Object.notify()相似 void signal(); //唤醒 await()等待队列中所有的线程。与object.notifyAll()相似 void signalAll(); }
说明:
1. ArrayBlockingQueue继承于AbstractQueue,并且它实现了BlockingQueue接口。
2. ArrayBlockingQueue内部是通过Object[]数组保存数据的,也就是说ArrayBlockingQueue本质上是通过数组实现的。ArrayBlockingQueue的大小,即数组的容量是创建ArrayBlockingQueue时指定的。
3. ArrayBlockingQueue与ReentrantLock是组合关系,ArrayBlockingQueue中包含一个ReentrantLock对象(lock)。ReentrantLock是可重入的互斥锁,ArrayBlockingQueue就是根据该互斥锁实现“多线程对竞争资源的互斥访问”。而且,ReentrantLock分为公平锁和非公平锁,关于具体使用公平锁还是非公平锁,在创建ArrayBlockingQueue时可以指定;而且,ArrayBlockingQueue默认会使用非公平锁。
4. ArrayBlockingQueue与Condition是组合关系,ArrayBlockingQueue中包含两个Condition对象(notEmpty和notFull)。而且,Condition又依赖于ArrayBlockingQueue而存在,通过Condition可以实现对ArrayBlockingQueue的更精确的访问 :(01)若某线程(线程A)要取数据时,数组正好为空,则该线程会执行notEmpty.await()进行等待;当其它某个线程(线程B)向数组中插入了数据之后,会调用notEmpty.signal()唤醒“notEmpty上的等待线程”。此时,线程A会被唤醒从而得以继续运行。
(02)若某线程(线程H)要插入数据时,数组已满,则该线程会它执行notFull.await()进行等待;当其它某个线程(线程I)取出数据之后,会调用notFull.signal()唤醒“notFull上的等待线程”。此时,线程H就会被唤醒从而得以继续运行。
5.在调用await()方法前线程必须获得重入锁,调用await()方法后线程会释放当前占用的锁。同理在调用signal()方法时当前线程也必须获得相应重入锁,调用signal()方法后系统会从condition.await()等待队列中唤醒一个线程。当线程被唤醒后,它就会尝试重新获得与之绑定的重入锁,一旦获取成功将继续执行。所以调用signal()方法后一定要释放当前占用的锁,这样被唤醒的线程才能有获得锁的机会,才能继续执行。
2.源码分析
2.1主要属性
/** The queued items ,fianl修饰,比volatile更严格*/ 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;
2.2主要构造方法
/** * Creates an {@code ArrayBlockingQueue} with the given (fixed) * capacity and default access policy(非公平). * * @param capacity the capacity of this queue * @throws IllegalArgumentException if {@code capacity < 1} */ public ArrayBlockingQueue(int capacity) { this(capacity, false); //非公平锁 } /** * Creates an {@code ArrayBlockingQueue} with the given (fixed) * capacity and the specified access policy. * * @param capacity the capacity of this queue * @param fair if {@code true} then queue accesses for threads blocked * on insertion or removal, are processed in FIFO order; * if {@code false} the access order is unspecified. * @throws IllegalArgumentException if {@code capacity < 1} */ public ArrayBlockingQueue(int capacity, boolean fair) { if (capacity <= 0) throw new IllegalArgumentException(); this.items = new Object[capacity]; //fair是“可重入的独占锁(ReentrantLock)”的类型。fair为true,表示是公平锁;fair为false,表示是非公平锁。在公平的锁中,如果有另一个线程持有锁或者有其他线程在等待队列中等待这个所,那么新发出的请求的线程将被放入到队列中。而非公平锁上,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中(此时和公平锁是一样的)。所以,它们的差别在于非公平锁会有更多的机会去抢占锁。 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 { final Object[] items = this.items; int i = 0; try { for (E e : c) items[i++] = Objects.requireNonNull(e); } catch (ArrayIndexOutOfBoundsException ex) { throw new IllegalArgumentException(); } count = i; putIndex = (i == capacity) ? 0 : i; //putIndex表示当前有多少个数据,takeIndex表示从队列拿出来的数量 } finally { lock.unlock(); } }
Lock的作用是提供独占锁机制,来保护竞争资源;而Condition是为了更加精细的对锁进行控制,它依赖于Lock,通过某个条件对多线程进行控制。
2.3入队
入队有四个方法,它们分别是add(E e)、offer(E e)、put(E e)、offer(E e, long timeout, TimeUnit unit),它们有什么区别呢?
public boolean add(E e) { return super.add(e); } //offer方法是立刻返回的,它并不像其他方法那样,当队列满时会一直等待。 public boolean offer(E e) { Objects.requireNonNull(e); final ReentrantLock lock = this.lock; lock.lock(); try { if (count == items.length) //这里count是不是有某种限制,使得不能超过items.length return false; //立即返回 else { enqueue(e); return true; } } finally { lock.unlock(); } } /** * Inserts element at current put position, advances, and signals. * Call only when holding lock. */ private void enqueue(E e) { // assert lock.isHeldByCurrentThread(); // assert lock.getHoldCount() == 1; // assert items[putIndex] == null; final Object[] items = this.items; items[putIndex] = e; // 如果放指针到数组尽头了,就返回头部 if (++putIndex == items.length) putIndex = 0; // 元素数量加1 count++; // 唤醒notEmpty,因为入队了一个元素,所以肯定不为空了 notEmpty.signal(); } /** * Inserts the specified element at the tail of this queue, waiting * up to the specified wait time for space to become available if * the queue is full. * * @throws InterruptedException {@inheritDoc} * @throws NullPointerException {@inheritDoc} */ public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException { Objects.requireNonNull(e); long nanos = unit.toNanos(timeout); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { // 如果数组满了,就阻塞nanos纳秒 // 如果唤醒这个线程时依然没有空间且时间到了就返回false //这里之所以使用while而不是if是因为有可能多个线程阻塞在lock上,即使唤醒了可能其它线程先一步修改了队列又变成满的了,这时候需要再次等待或者纳秒数耗尽直接退出 while (count == items.length) { if (nanos <= 0L) return false; nanos = notFull.awaitNanos(nanos); } enqueue(e); return true; } finally { lock.unlock(); } } /** * Inserts the specified element at the tail of this queue, waiting * for space to become available if the queue is full. * * @throws InterruptedException {@inheritDoc} * @throws NullPointerException {@inheritDoc} */ public void put(E e) throws InterruptedException { Objects.requireNonNull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { // 如果数组满了,使用notFull等待 // notFull等待的意思是说现在队列满了 // 只有取走一个元素后,队列才不满 // 然后唤醒notFull,然后继续现在的逻辑 // 这里之所以使用while而不是if // 是因为有可能多个线程阻塞在lock上 // 即使唤醒了可能其它线程先一步修改了队列又变成满的了 // 这时候需要再次等待 while (count == items.length) notFull.await(); enqueue(e); } finally { lock.unlock(); } }
(1)add(e)时如果队列满了则抛出异常;
(2)offer(e)时如果队列满了则返回false;
(3)put(e)时如果队列满了则使用notFull等待;
(4)offer(e, timeout, unit)时如果队列满了则等待一段时间后如果队列依然满就返回false;
(5)利用指针循环使用数组来存储元素;
出队
出队有四个方法,它们分别是remove()、poll()、take()、poll(long timeout, TimeUnit unit),它们有什么区别呢?
//删除基于循环数组的队列中的内部元素本质上是一个缓慢且具有破坏性的操作,所以应该只在特殊情况下执行,
//理想情况下,只有当已知队列不能被其他线程访问时才执行。
public boolean remove(Object o) {
if (o == null) return false;
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count > 0) {
final Object[] items = this.items;
//本次查询分两种情况。第一种情况是takeIndex < putIndex,此时查询终点是putIndex,
//第二种情况是takeIndex >= putIndex,此时查询分两段:takeIndex-item.length、0-putIndex,
//原因是该队列是一个循环队列,当putIndex到达数组容量上限时回返回到开头
for (int i = takeIndex, end = putIndex,
to = (i < end) ? end : items.length;
; i = 0, to = end) {
for (; i < to; i++)
if (o.equals(items[i])) {
removeAt(i);
return true;
}
if (to == end) break;
}
}
return false;
} finally {
lock.unlock();
}
}
// 这个方法是不阻塞的,当队列未空的时候,直接返回null值,所以实现中只是一个锁的简单使用,防止并发问题
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 如果队列无元素,则阻塞等待nanos纳秒
// 如果下一次这个线程获得了锁但队列依然无元素且已超时就返回null
while (count == 0) {
if (nanos <= 0L)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
return dequeue(); //添加与删除的返回值不同,offer()返回布尔值,而poll()返回E或null
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
/**
* Extracts element at current take position, advances, and signals.
* Call only when holding lock.
*/
private E dequeue() {
// assert lock.isHeldByCurrentThread();
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
// 取取指针位置的元素,能这样做的原因是外部调用dequeue()的take()与poll()都获取了锁,并且通过判断知道数组中元素数量count不为0
E e = (E) items[takeIndex];
// 把取指针位置设为null
items[takeIndex] = null;
// 取指针后移,如果数组到头了就返回数组前端循环利用
if (++takeIndex == items.length) takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued(); //取出元素可能使得该数组上的迭代器发生变化,所以同通知各个迭代器并作出相应动作
//唤醒notFull条件
notFull.signal();
return e;
}
void removeAt(final int removeIndex) {
// assert lock.isHeldByCurrentThread();
// assert lock.getHoldCount() == 1;
// assert items[removeIndex] != null;
// assert removeIndex >= 0 && removeIndex < items.length;
final Object[] items = this.items;
if (removeIndex == takeIndex) {
// removing front item; just advance
items[takeIndex] = null;
if (++takeIndex == items.length) takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
} else {
// an "interior" remove
// slide over all others up through putIndex.
for (int i = removeIndex, putIndex = this.putIndex;;) {
int pred = i;
if (++i == items.length) i = 0;
if (i == putIndex) {
items[pred] = null;
this.putIndex = pred;
break;
}
items[pred] = items[i];
}
count--;
if (itrs != null)
itrs.removedAt(removeIndex);
}
notFull.signal(); //唤醒添加线程
}
(1)remove()时如果队列为空则抛出异常;
(2)poll()时如果队列为空则返回null;
(3)take()时如果队列为空则阻塞等待在条件notEmpty上;
(4)poll(timeout, unit)时如果队列为空则阻塞等待一段时间后如果还为空就返回null;
(5)利用取指针循环从数组中取元素
跟操作阻塞队列代码似乎看完了,还剩余一些查询的,基本是修改阻塞队列的时候加上锁,排除锁的代码就是在操作一个数组环形队列,但我们可以在思考一些问题:
如何判断队列的大小?count ?
如何判断当前将要出队的元素 items[takeIndex]?
我之所以加上? 是因为考虑我在判断队列大小或者出队元素的时候其它线程有可能正在修改队列这个时候会差生一下影响,看下JDK是如何实现的?
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return itemAt(takeIndex); // null when queue is empty
} finally {
lock.unlock();
}
}
public int size() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
//返回此队列在理想情况下(在没有内存或资源约束的情况下)可以不阻塞地接受的附加元素的数量。
//这总是等于这个队列的初始容量减去这个队列的当前{@code size}.注意,您不能总是通过检查
//{@coderemainingCapacity}来判断插入元素的尝试是否会成功,因为可能是另一个线程将要插入或
//删除一个元素。length表示数组的长度,是数组的属性
public int remainingCapacity() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return items.length - count;
} finally {
lock.unlock();
}
}
public boolean contains(Object o) {
if (o == null) return false;
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count > 0) {
final Object[] items = this.items;
for (int i = takeIndex, end = putIndex,
to = (i < end) ? end : items.length;
; i = 0, to = end) {
for (; i < to; i++)
if (o.equals(items[i]))
return true;
if (to == end) break;
}
}
return false;
} finally {
lock.unlock();
}
}
总结
(1)ArrayBlockingQueue是有界的阻塞队列,不接受null 。底层数据接口是数组,下标putIndex/takeIndex,构成一个环形FIFO队列
(2)ArrayBlockingQueue利用takeIndex和putIndex循环利用数组;所有的增删改查数组公用了一把锁ReentrantLock和两个条件,入队和出队数组下标和count变更都是靠这把锁来维护安全的。
(3)ArrayBlockingQueue不需要扩容,因为是初始化时指定容量,并循环利用数组;
(4)阻塞的场景:1获取lock锁,2进入和取出还要满足condition 满了或者空了都等待出队和加入唤醒,ArrayBlockingQueue我们主要是put和take真正用到的阻塞方法(条件不满足)。
(5)成员cout /putIndex、takeIndex是共享的,所以一些查询方法size、peek、toString、方法也是加上锁保证线程安全,但没有了并发损失了性能。
(6)remove(Object obj) 返回了第一个equals的Object
(7)队和出队各定义了四组方法为满足不同的用途;
参考:
https://www.cnblogs.com/tong-yuan/p/ArrayBlockingQueue.html
https://www.cnblogs.com/kexianting/p/8550598.html
https://blog.csdn.net/mayongzhan_csdn/article/details/80888655