在上一篇DelayQueue的文章【延迟队列DelayQueue的应用】中,我在初始化系统时将所有DelayTask对象入队,然后开启一个消费者线程循环调用DelayQueue的take()方法取出元素消费,后续系统运行过程中若有生产出新的DelayTask,便调用DelayQueue的put()或add()方法将元素入队,从而达到延时执行的目的
在实践中,遇到几个问题,在此记录一下:
问题1:
当DelayQueue队列中没有元素时,这时入队的元素便位于队列头部,阅读DelayQueue文档,可以看到take()方法:获取并移除头部,在可从此队列获得到期延迟的元素之前一直等待
因此当队列中只有一个元素时,消费者线程便会根据这个元素的到期时间,进入阻塞状态,假设这个元素的到期时间为60s,那么线程将在60s之后唤醒,那么在60s到期之前,假设新入队一个到期时间为40s的元素,虽然40s比60s先到期,但此时线程已进入阻塞,还是会在60s后进行唤醒,60s后,会先取出60s到期的那个元素进行消费,然后取出40s到期的元素进行消费,那么就会造成这个40s到期的元素任务超时了20s
这是因为在系统初始化的时候便创建了一个消费者线程,因此一旦队列中出现了元素,便会立马去take()、进入阻塞,后续入队的元素虽然会调用Delayed接口中的compareTo()方法根据过期时间来排序,但也不会让已经进入阻塞的消费者线程重新来获取此时过期时间最短的元素
参考以下demo:
元素1过期时间为60s,首先进入队列,消费者线程进入阻塞状态
元素2过期时间为40s,为第二个进入队列的元素
元素3过期时间为10s,为第三个进入队列的元素,入队时会进行排序,元素3位于第二的位置,元素2位于队尾,元素1还是头部
(假设处理延时任务很快,接近于0)
60s后,消费者线程消费元素1,元素1出队
消费完元素1后,立马依次take()取出元素3、元素2进行消费,但是可以看到,元素3超时了50s,元素2超时了20s
Demo代码如下:
实现Delayed接口的DelayTask
@Data
public class DelayTask implements Delayed {
/**
* 开始计时时间 不设置则默认为当前系统时间
*/
private transient Date taskStartTime = new Date();
/**
* 过期时间 不设置则默认1分钟
*/
private transient long taskExpiredTime = 60 * 1000;
/**
* 初始设置开始计时时间
* taskStartTime 开始时间 [String] [yyyy-MM-dd HH:mm:ss]
* taskExpiredTime 过期时间 [long] 单位:s
* @param taskStartTime
* @param taskExpiredTime
*/
public void initTaskTime(String taskStartTime, long taskExpiredTime) {
if(Assert.notEmpty(taskStartTime)) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
this.taskStartTime = sdf.parse(taskStartTime);
} catch (ParseException e) {
e.printStackTrace();
}
}
this.taskExpiredTime = taskExpiredTime;
this.taskExpiredTime += this.taskStartTime.getTime();
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(taskExpiredTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
return (this.getDelay(TimeUnit.MILLISECONDS) - ((DelayTask) o).getDelay(TimeUnit.MILLISECONDS)) > 0 ? 1:0;
}
}
元素类:
@Data
public class Element extends DelayTask {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private String name;
private Date startTime;
public void execute() {
Date execteTime = new Date();
long timeout = (execteTime.getTime() - startTime.getTime()) / 1000;
System.out.println(name + "执行任务,执行时间:" + sdf.format(execteTime) + ",超时:" + timeout + "s");
}
}
DelayQueue工具类:
public class DelayQueueHelper {
private volatile static DelayQueueHelper delayQueueHelper = null;
private static DelayQueue<DelayTask> queue = null;
private DelayQueueHelper() {
}
public static DelayQueueHelper getInstance() {
if(delayQueueHelper == null) {
synchronized(DelayQueueHelper.class) {
delayQueueHelper = new DelayQueueHelper();
}
}
if(queue == null) {
queue = new DelayQueue<>();
}
return delayQueueHelper;
}
public void addTaskV1(DelayTask task) {
queue.add(task);
}
public DelayQueue<DelayTask> getQueue() {
return queue;
}
}
测试类:
public class DelayQueueRunnerDemo {
public static void main(String[] args) {
testExecute();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
DelayQueueHelper queueHelper = DelayQueueHelper.getInstance();
Date now = new Date();
System.out.println("系统当前时间:" + sdf.format(now));
Calendar calendar = Calendar.getInstance();
calendar.setTime(now);
//元素1延迟60s执行
calendar.add(Calendar.SECOND, 60);
Element e1 = new Element();
e1.setName("元素1");
Date startTime_1 = calendar.getTime();
e1.setStartTime(startTime_1);
long expiredTime = startTime_1.getTime() - now.getTime();
e1.initTaskTime(sdf.format(now), expiredTime);
//元素2延迟40s执行
calendar.add(Calendar.SECOND, -20);
Element e2 = new Element();
e2.setName("元素2");
Date startTime_2 = calendar.getTime();
e2.setStartTime(startTime_2);
expiredTime = startTime_2.getTime() - now.getTime();
e2.initTaskTime(sdf.format(now), expiredTime);
//元素3延迟10s执行
calendar.add(Calendar.SECOND, -30);
Element e3 = new Element();
e3.setName("元素3");
Date startTime_3 = calendar.getTime();
e3.setStartTime(startTime_3);
expiredTime = startTime_3.getTime() - now.getTime();
e3.initTaskTime(sdf.format(now), expiredTime);
queueHelper.addTaskV1(e1);
System.out.println("元素1入队,到期时间:" + sdf.format(startTime_1));
queueHelper.addTaskV1(e2);
System.out.println("元素2入队,到期时间:" + sdf.format(startTime_2));
queueHelper.addTaskV1(e3);
System.out.println("元素3入队,到期时间:" + sdf.format(startTime_3));
}
public static void testExecute() {
new Thread(() -> {
while(true) {
try {
Element element = (Element) DelayQueueHelper.getInstance().getQueue().take();
element.execute();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
测试输出:
系统当前时间:2022-09-27 16:26:04
元素1入队,到期时间:2022-09-27 16:27:04
元素2入队,到期时间:2022-09-27 16:26:44
元素3入队,到期时间:2022-09-27 16:26:14
元素1执行任务,执行时间:2022-09-27 16:27:04,超时:0s
元素3执行任务,执行时间:2022-09-27 16:27:04,超时:49s
元素2执行任务,执行时间:2022-09-27 16:27:04,超时:19s
对于问题1,系统初始化时,可以先将所有要入队的元素按到期时间排序,然后入队,保证入队的时候,第一个入队的元素的到期时间是最短的
问题2:
当系统初始化、消费者线程启动后,在后续往延迟队列中添加元素时,若此时队列中没有元素,则又会出现了以上问题,若此时队列中有元素,消费者线程阻塞当中,假设队列中元素如下,并要将元素4入队:
入队后:
当30s后,消费者线程去消费元素4,已经超时20s
解决:
入队时,先判断入队元素的到期时间是否小于头部元素,若小于,则clear()清空队列,先将入队元素入队,然后依次将原来的元素入队,以保证头部元素的到期时间最短
public void addTaskV2(DelayTask task) {
DelayQueue<DelayTask> delayQueue = queue;
if(Assert.notEmpty(delayQueue)) {
DelayTask queueObj = delayQueue.peek();
if(queueObj.getTaskExpiredTime() > task.getTaskExpiredTime()) {
DelayQueue<DelayTask> dataQueue = new DelayQueue<>();
dataQueue.addAll(delayQueue);
delayQueue.clear();
delayQueue.add(task);
for(Iterator<DelayTask> iterator = dataQueue.iterator(); iterator.hasNext();) {
delayQueue.add(iterator.next());
}
}else {
delayQueue.add(task);
}
}else {
delayQueue.add(task);
}
}
使用的入队方法,执行后:
系统当前时间:2022-09-27 17:00:13
元素1入队,到期时间:2022-09-27 17:01:13
元素2入队,到期时间:2022-09-27 17:00:53
元素3入队,到期时间:2022-09-27 17:00:23
元素3执行任务,执行时间:2022-09-27 17:00:23,超时:0s
元素2执行任务,执行时间:2022-09-27 17:00:53,超时:0s
元素1执行任务,执行时间:2022-09-27 17:01:13,超时:0s