1. 队列
队列是一种先进先出(First In First Out)的数据结构。
可以分成阻塞和非阻塞两类:
- 阻塞队列,即如果队列已满,入队操作会阻塞;如果队列为空,出队操作也会阻塞。(Blocking标识)
- 非阻塞队列,即如果队列已满,执行入队操作会直接返回;如果队列为空,执行出队操作也是直接返回。
也可以分成单端和双端两类:
- 单端队列,即只能在队尾入队,队首出队;(Queue标识)
- 双端队列,队首队尾都可以出队。(Deque标识)
单端阻塞队列:主要有ArrayBlockingQueue(内部是数组)、LinkedBlockingQueue(内部是链表)、PriorityBlockingQueue(优先级队列)等。
双端阻塞队列:主要有LinkedBlockingDeque(内部是链表)。
单端非阻塞队列:ConcurrentLinkedQueue。
双端非阻塞队列:ConcurrentLinkedDeque。
ArrayBlockingQueue和LinkedBlockingQueue是有界队列,工作中建议使用有界队列,防止数据量过大时出现OOM。
本文主要介绍ArrayBlockingQueue的代码实现。
2. ArrayBlockingQueue
2.1 继承关系
ArrayBlockingQueue继承了抽象类AbstractQueue,继承其中的一些方法。并且实现了BlockingQueue接口,实现了阻塞队列所需实现的一些方法。
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable
2.2 属性
(1)Object数组,用于存储数据。 数组容量在构造实例时由用户确定,不可扩容。
final Object[] items;
(2)队头位置
int takeIndex;
(3)队尾位置
int putIndex;
(4)队列中的元素数
int count;
(5)可重入锁,即线程可以重复获取的锁。比如:当某一个线程获取了锁,然后执行临界代码,执行过程中又遇到这把锁,如果该锁是可重入锁,此时可以加锁成功;否则,就会阻塞。锁具有不可变性,所以用final修饰。
final ReentrantLock lock;
(6)条件变量notEmpty。当从队列中取元素时,如果不满足条件notEmpty,就不能成功获取。条件变量也是不可变的,所以用final修饰。
private final Condition notEmpty;
(7)条件变量notFull。当添加元素到队列中时,如果不满足条件notFull,就不能成功添加。
private final Condition notFull;
注意:Lock和Condition是java中管程的实现方式,管程实现原理可参考本人的另一篇博文:https://blog.csdn.net/Longstar_L/article/details/109489227
2.3 构造器
(1)设置队列长度的构造器,调用了(2)中的构造器,fair默认为false。
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
(2)设置队列长度,并且可以自定义fair的构造器。
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
//分配数组空间,长度为capacity
this.items = new Object[capacity];
//创建可重入锁实例
lock = new ReentrantLock(fair);
//创建2个条件变量
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
fair指的是队列中所有的可重入锁是公平锁还是非公平锁。
- 公平锁:当有线程释放锁,会从阻塞队列中唤醒一个等待时间最久的线程。
- 非公平锁:不提供公平保证,可能刚到来的线程会直接获取到锁。
2.4 入队
(1)**add(E e)**方法在队尾插入一个元素。如果队列已满,会直接抛出异常。属于非阻塞方法。
public boolean add(E e) {
//调用父类的add方法
return super.add(e);
}
public boolean add(E e) {
if (offer(e))
return true;
else
//如果队列已满,添加失败,就会直接抛出异常
throw new IllegalStateException("Queue full");
}
最终调用的还是本类实现的offer方法。
public boolean offer(E e) {
//判断e是否是null
checkNotNull(e);
//拿到可重入锁
final ReentrantLock lock = this.lock;
//try...finally...的模式加锁和解锁
lock.lock();
try {
//如果队列已满,返回false
if (count == items.length)
return false;
else {
//否则,入队
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
final Object[] items = this.items;
//队尾放上x
items[putIndex] = x;
//队尾后移一位。采用的是环形队列,如果队尾超出了边界,就回到起始位置
if (++putIndex == items.length)
putIndex = 0;
//总元素数加1
count++;
//notEmpty的阻塞队列中唤醒一个线程,重新回到管程入口等待入口锁
notEmpty.signal();
}
(2)offer(E)方法在队列已满的情况下,直接返回false,不抛异常。属于非阻塞方法。
(3)put方法采用lock+condition的管程方式。属于阻塞方法
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
//可中断地加锁
lock.lockInterruptibly();
try {
//如果队列满了,就将当前线程放入条件变量notFull的阻塞队列,
//当有其他线程执行notFull.signal,就会从该阻塞队列中唤醒一个线程回到管程入口处再次尝试获取锁,
//当获取到锁后,会在await方法处返回,
//必须使用while循环重新验证该条件是否还满足
while (count == items.length)
notFull.await();
//重新验证队列不满,就可以执行入队操作
enqueue(e);
} finally {
lock.unlock();
}
}
lock.lockInterruptibly()说明:如果一个线程获取了锁lock之后,在获取另一把锁时又阻塞了,现在我们给该阻塞线程发送中断信号,可以唤醒它,就有机会释放其已经占有的锁A。
(4)offer(E e, long timeout, TimeUnit unit)方法是管程+支持超时的入队方法。属于阻塞+超时方法。
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
checkNotNull(e);
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
//可中断地加锁
lock.lockInterruptibly();
try {
while (count == items.length) {
//如果超时了,直接返回false
if (nanos <= 0)
return false;
//将剩余等待时间传入,返回时会得到新的剩余时间
nanos = notFull.awaitNanos(nanos);
}
enqueue(e);
return true;
} finally {
lock.unlock();
}
}
2.5 出队
(1)take方法,属于阻塞方法。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//队列为空时,在notEmpty的阻塞队列中等待
while (count == 0)
notEmpty.await();
//返回出队元素
return dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
final Object[] items = this.items;
@SuppressWarnings("unchecked")
//获取队头元素
E x = (E) items[takeIndex];
//清空该位置
items[takeIndex] = null;
//队头后移一位,如果越界了,就回到起始位置
if (++takeIndex == items.length)
takeIndex = 0;
//元素数减1
count--;
if (itrs != null)
itrs.elementDequeued();
//notFull的阻塞队列中唤醒一个线程
notFull.signal();
//返回出队元素
return x;
}
(2)poll方法在队列为空时,直接返回null,属于非阻塞方法。
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
2.6 查找队头元素
peek()方法。如果队列为空,直接返回null。
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return itemAt(takeIndex); // null when queue is empty
} finally {
lock.unlock();
}
}
2.7 获取队列的元素数
public int size() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
2.8 小结
- ArrayBlockingQueue总体上采用Lock+Condition的管程方式。
- 所有的插入、删除、查找方式都使用同一把锁ReentrantLock。
- 设置两个条件变量notFull和notEmpty,来同步线程间的入队、出队操作,防止队空时出队、队满时入队。