1 队列的设计架构
- 队列本质也是一种集合,用来存储数据的
- 队列除了基础的集合特性外还有一些特殊的性质,如:从头部弹出数据,往尾部插入数据
- 阻塞队列多了一些插入,取出数据阻塞策略的逻辑
接下来我们以ArrayBlockingQueue为例,看下阻塞队列的一些具体实现。
2 ArrayBlockingQueue为例
看类名就知道,这是使用数组为容器来实现的队列。
2.1 构造方法
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();
}
- 参数capacity,队列的长度,不允许小于1
- fair,用于ReentrantLock是否创建公平锁,这里注意对于队列中的数据来说是先进先出的,公平与非公平针对的是等待获取队列中数据的线程,是不是先获取到锁的就先获取数据。
2.2 offer方法
使用锁保证单线程往队列中添加一个对象,如果队列已满返回false,添加成功返回true,区别于add方法。
public boolean offer(E e) {
//校验入参是否为空
checkNotNull(e);
//获取当前队列创建的锁对象
final ReentrantLock lock = this.lock;
//上锁
lock.lock();
try {
//队列以满,返回false
if (count == items.length)
return false;
else {
//往队列中添加数据
enqueue(e);
return true;
}
} finally {
//解锁
lock.unlock();
}
}
offer的重载方法,等待指定时间,如果指定时间后队列还是满的不能往里面插入值就返回false。
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
//校验入参
checkNotNull(e);
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
//区别于lock方法,该方法会在线程中断时立即抛出异常,不会再去获取锁
lock.lockInterruptibly();
try {
while (count == items.length) {
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos);
}
enqueue(e);
return true;
} finally {
lock.unlock();
}
}
2.3 add方法
调用父类中的add方法。
public boolean add(E e) {
return super.add(e);
}
最终还是调用的offer方法,不过如果队列满了会抛出异常。区别于offer方法添加失败返回false
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
2.4 put方法
put方法是个阻塞方法,会一直尝试往队列中添加数据直到添加成功。
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();
}
}
2.5 enqueue方法
数组为0处开始添加数据,使用全局变量putIndex标记下一次添加数据的位置,使用全局变量count标记队列中数据的条数,如果添加到数组末位置,则从0位置开始继续添加(取数也是从0开始取),如此反复。
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
//获取当前队列中的数组对象
final Object[] items = this.items;
//将当前对象放入指定坐标的数组中
items[putIndex] = x;
//坐标后移一位,如果大小和数组长度相等,坐标设置成1
if (++putIndex == items.length)
putIndex = 0;
//队列中数组
count++;
//唤醒一个因调用take方法进入等待队列中的线程进入同步队列
notEmpty.signal();
}
2.6 poll方法
从队列中获取数据,如果队列中没有数据直接返回空值。
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
重载方法,等待指定的时间,如果指定的时间内,队列中没有数据则返回null。
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) {
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
return dequeue();
} finally {
lock.unlock();
}
}
2.7 take方法
从队列中获取数据,如果队列为空,当前线程进入等待队列。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
2.8 dequeue方法
private E dequeue() {
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;
}
2.9 peek方法
从队列中获取头部数据,区别于take和poll方法,该方法只获取不会改变队列长度。也就是说不会从队列中删除获取的这条数据。
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return itemAt(takeIndex);
} finally {
lock.unlock();
}
}
3 总结一下
队列遵循先进先出的逻辑,只是针对队列中的数据。不保证先获取数据的线程就一定先获取到数据。
获取和插入数据都有对应的阻塞和非阻塞方法。本篇浅显的了解一下队列的常用方法和基本逻辑。
阻塞队列的实现基于锁和Codition,后面我们来看这一部分内容。