三十二、阻塞队列之DelayQueue

78 阅读6分钟

阻塞队列之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存放元素方法

  1. 加锁
  2. 将数据放入队列中,队列中数据重新排序,将时间最短的数据放在首部
  3. 判断队列首部的数据是否是此数据,如果是,唤醒消费者,刷新消费者的等待时间
  4. 释放锁
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无参方法

  1. 加锁
  2. 复制队列首部数据
  3. 判断数据是否为空,或者数据的时间是否已过
  4. 如果数据为空,或者数据的时间未过,返回null
  5. 如果数据不为空,并且数据的时间已过,取出数据
  6. 释放锁
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有参方法

  1. 加锁
  2. 进入for循环,复制队列首部数据
  3. 判断数据是否为空,如果为空,判断线程等待时间是否已过
  4. 如果已过,返回null,否则,线程阻塞有一段时间,并且释放锁
  5. 如果数据不为空,判断数据延迟时间是否已过,如果已过,返回数据
  6. 如果未过,判断线程等待时间是否已过,如果已过,返回null
  7. 如果线程等待时间未过,判断线程等待时间是否小于数据延迟时间
  8. 如果小于,线程阻塞一段时间,并且释放锁
  9. 如果大于等于,判断leader线程是否不为空
  10. 如果不为空,线程阻塞一段时间,并且释放锁
  11. 如果为空,此线程设置为leader线程,并且阻塞一段时间,和释放锁
  12. 线程被唤醒,尝试获取锁,获取锁后,重新计算线程等待时间,leader设置为null,重复2-12步
  13. 拿到数据准备返回,判断队列中是否有数据,如果有,唤醒消费者
  14. 释放锁
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方法

  1. 加锁
  2. 进入for循环,复制队列首部数据
  3. 判断数据是否为空,如果为空,线程阻塞,并且释放锁
  4. 如果数据不为空,判断数据延迟时间是否已过,如果已过,返回数据
  5. 如果未过,判断leader线程是否不为空
  6. 如果不为空,线程阻塞,并且释放锁
  7. 如果为空,此线程设置为leader线程,并且阻塞和释放锁
  8. 线程被唤醒,尝试获取锁,获取锁后,leader设置为null,重复2-8步
  9. 拿到数据准备返回,判断队列中是否有数据,如果有,唤醒消费者
  10. 释放锁
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();
	}
}