阻塞队列之DelayQueue
基础概念
DelayQueue是一个延迟队列,存储的对象必须实现Delayed接口,存放需要在队列中存放的时间长度信息
DelayQueue是基于PriorityQueue实现的,也是二叉堆结构,所以存储的对象也需要实现Comparable接口,比较的是对象的存放时间
DelayQueue的定义:
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E>
Delayed的定义:
public interface Delayed extends Comparable<Delayed> {
// 这个方法的实现逻辑是
// 用对象的到期时间减去当前时间,算出剩余时间
long getDelay(TimeUnit unit);
}
编写一个存入DelayQueue的对象类:
public class task implements Delayed {
private String name;
private Long ti
me;
public task(String name, Long delay) {
this.name = name;
this.time = System.currentTimeMillis() + delay;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.time - System.currentTimeMillis(), unit);
}
@Override
public int compareTo(Delayed o) {
return (int) (this.time - ((task) o).getTime());
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getTime() {
return time;
}
public void setTime(Long time) {
this.time = time;
}
}
基本属性
// 阻塞队列的锁,必有的东东
private final transient ReentrantLock lock = new ReentrantLock();
// DelayQueue是基于PriorityQueue实现的
private final PriorityQueue<E> q = new PriorityQueue<E>();
// 用于存储等待栈顶数据的消费者,在整体写入和消费的过程中,会涉及到leader的一些判断
private Thread leader = null;
// DelayQueue是基于PriorityQueue实现的,相当于无界队列,生产者是不会阻塞的。
// Condition是给消费者使用的
// 例如消费者在获取数据时,发现数据还没过延迟时间,则线程需要挂起。
// 如果生产者存入的数据移动到了栈顶,此时生产者会唤醒消费者
private final Condition available = lock.newCondition();
DelayQueue存放元素方法
- 加锁
- 将数据放入队列中,队列中数据重新排序,将时间最短的数据放在首部
- 判断队列首部的数据是否是此数据,如果是,唤醒消费者,刷新消费者的等待时间
- 释放锁
public boolean offer(E e) {
// 生产者和消费者共用的一把锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 调用PriorityQueue的offer方法存放数据
// 会比较元素的存放时间,时间短的放在栈顶位置
q.offer(e);
// 判断栈顶元素是否是当前元素
if (q.peek() == e) {
leader = null;
// 栈顶元素是当前元素,唤醒消费者
// 让消费者重新等待,刷新消费者的等待时间
available.signal();
}
return true;
} finally {
lock.unlock();
}
}
DelayQueue获取元素方法
remove方法
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
poll无参方法
- 加锁
- 复制队列首部数据
- 判断数据是否为空,或者数据的时间是否已过
- 如果数据为空,或者数据的时间未过,返回null
- 如果数据不为空,并且数据的时间已过,取出数据
- 释放锁
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek();
if (first == null || first.getDelay(NANOSECONDS) > 0)
// 队列为空,或者栈顶元素没过延迟时间
return null;
else
// 栈顶元素已经过了延迟时间,可以获取
return q.poll();
} finally {
lock.unlock();
}
}
poll有参方法
- 加锁
- 进入for循环,复制队列首部数据
- 判断数据是否为空,如果为空,判断线程等待时间是否已过
- 如果已过,返回null,否则,线程阻塞有一段时间,并且释放锁
- 如果数据不为空,判断数据延迟时间是否已过,如果已过,返回数据
- 如果未过,判断线程等待时间是否已过,如果已过,返回null
- 如果线程等待时间未过,判断线程等待时间是否小于数据延迟时间
- 如果小于,线程阻塞一段时间,并且释放锁
- 如果大于等于,判断leader线程是否不为空
- 如果不为空,线程阻塞一段时间,并且释放锁
- 如果为空,此线程设置为leader线程,并且阻塞一段时间,和释放锁
- 线程被唤醒,尝试获取锁,获取锁后,重新计算线程等待时间,leader设置为null,重复2-12步
- 拿到数据准备返回,判断队列中是否有数据,如果有,唤醒消费者
- 释放锁
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
// 拿到等待时间
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
// 拿到栈顶元素
E first = q.peek();
if (first == null) {
if (nanos <= 0)
// 队列中没有数据并且线程等待时间已过,返回null
return null;
else
// 队列中没有数据并且线程等待时间没过,等待数据添加
nanos = available.awaitNanos(nanos);
} else {
// ----------------------队列不为空的情况--------------------------
// 拿到栈顶元素的剩余延迟时间
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
// 延迟时间已过,返回栈顶元素
return q.poll();
if (nanos <= 0)
// 延迟时间没过,但是线程等待时间已过,返回null
return null;
// -----------栈顶元素延迟时间没过,线程的等待时间也没过的情况---------
first = null;
if (nanos < delay || leader != null)
// 线程等待时间小于栈顶元素延迟时间
// 或者leader线程不为null
// 当前线程继续挂起,等待leader为null并且新数据的添加
// 获取数据时,leader线程优先
nanos = available.awaitNanos(nanos);
// -----leader线程为null,并且线程等待时间大于栈顶元素延迟时间------
else {
Thread thisThread = Thread.currentThread();
// 将leader设置成当前线程
leader = thisThread;
try {
// 等待栈顶元素延迟时间过去
long timeLeft = available.awaitNanos(delay);
// 计算出线程剩余的等待时间,因为可能是生产者存放新数据时唤醒的消费者
// 此时新数据可能存放到栈顶,需要消费者更新挂起的时间
nanos -= delay - timeLeft;
} finally {
if (leader == thisThread)
// 线程拿到数据,需要将leader设置为null
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
// leader线程已经取走数据了,需要唤醒其他消费者
// 可能需要消费者将挂起时间更新为栈顶数据延迟时间
available.signal();
lock.unlock();
}
}
take方法
- 加锁
- 进入for循环,复制队列首部数据
- 判断数据是否为空,如果为空,线程阻塞,并且释放锁
- 如果数据不为空,判断数据延迟时间是否已过,如果已过,返回数据
- 如果未过,判断leader线程是否不为空
- 如果不为空,线程阻塞,并且释放锁
- 如果为空,此线程设置为leader线程,并且阻塞和释放锁
- 线程被唤醒,尝试获取锁,获取锁后,leader设置为null,重复2-8步
- 拿到数据准备返回,判断队列中是否有数据,如果有,唤醒消费者
- 释放锁
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek();
if (first == null)
available.await();
else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
return q.poll();
first = null;
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}