本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。
第一次参与这种写作活动,请各位大佬随意观看
DelayQueue
可以通过延时队列,执行定时任务.
将定时任务放入队列中,队列会自动排序,当时间为0(到达定时时间),会将该任务放到队列头部,那么我们就可以取出定时任务,然后进行操作.
DelayQueue是Delayed元素的一个无界阻塞队列,只有延迟期满才能从队列中提取元素,队列的头部就是延迟期满后保存时间最长的Delayed元素.如果延迟期没有满,队列头部就没有元素,poll返回null.
当元素的getDelay 方法返回一个小于等于0的值,就会发生到期.
实现Delayed接口需要实现两个方法
getDelay()这个方法是Delayed的方法
这个方法返回的就是延迟期,当延迟期满元素就会放到队列头部,可以对元素进行取出等操作.
compareTo() 这个方法是Comparable的方法
通过这个方法,将队列中的元素进行排序,通常将getDelay作为排序标准
方法
-
take(): 阻塞方法,直到获取延迟期满的元素并移除
-
put():将指定元素插入队列
- 和offer一样
-
offer(E):将指定元素插入队列:加锁
public boolean offer(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { q.offer(e); if (q.peek() == e) { leader = null; available.signal(); } return true; } finally { lock.unlock(); } } -
poll():获取并移除头部延迟期满的元素,如果没有延迟期满的元素,就返回null
-
peel():获取但不移除队列头部,如果队列为空,返回null
-
size(): 返回元素数
-
clear(): 自动移除队列中所有元素
实现
1.填充的元素需要实现Delayed接口,因此可以使实体类实现这个接口,也可以将实体类封装一层再实现,不会破坏实体类的功能性.
public class TaskDelayed implements Delayed {
private ServicePackTask packTask;
// 获得延迟时间,用 发送时间-当前时间
@Override
public long getDelay(TimeUnit unit) {
// return unit.convert((packTask.getPushTime().getTime() - System.currentTimeMillis()) / 1000 , TimeUnit.SECONDS);
return (this.packTask.getPushTime().getTime() - System.currentTimeMillis()) /1000;
}
// 进行延时队列内部的比较,延迟时间短的放在头部
@Override
public int compareTo(Delayed o) {
return (int) (this.getDelay(TimeUnit.SECONDS) - o.getDelay(TimeUnit.SECONDS));
}
}
2.实现生产者和消费者方法
public void provider(ServicePackTask servicePackTask) {
TaskDelayed taskDelayed = new TaskDelayed(servicePackTask);
queue.offer(taskDelayed);
}
生产者比较简单,只要调用方法,就会将元素放入队列中
消费者的话其实就是无限循环读取.首先先判断队列中是否有数据,如果没有就睡眠一段时间再循环(减少CPU压力).
如果队列不为空,就使用take方法(会自动阻塞线程直到有元素的延迟期满)取出元素并对元素进行操作.
public void consumer() {
while (true) {
if (0 == queue.size()) {
System.out.println("完结");
break;
}
TaskDelayed take = null;
try {
take = queue.take();
System.out.println(take.getPackTask().getPushTime());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3.封装方法
使用两个线程将生产和消费隔开.
为什么要将两个方法放到一起?
因为消费是队列自动计算的,但是如果我们不去触发,就无法对队列中的元素进行操作,所以要将消费放入一个可以出发的地方,就是生产的地方.
但是需要使用多线程将两个方法隔开,否则消费者将无法阻塞生产的方法.
public void start(ServicePackTask servicePackTask) {
new Thread(new Runnable() {
@Override
public void run() {
provider(servicePackTask);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
consumer();
}
}).start();
}
个人优化
如果每次调用生产都要创建,并且创建一个消费线程来进行消费,这样会大大增加服务器的负担,因此,我们可以通过将消费方法放入类的代码块中来执行,当创建类的实例时,就会触发代码块中的消费逻辑,开始进行消费.
{ // 队列消费:发送到用户中心和插入数据库
new Thread(new Runnable() {
@Override
public void run() {
ServicePackOrderPlanDO servicePackOrderPlanDO;
OrderPlanDelayItem take = null;
while (true) {
if (0 == queue.size()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
try {
take = queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (null != take) {
servicePackOrderPlanDO = take.getServicePackOrderPlanDO();
// 插入消息表
ServicePackSendMessageDO servicePackSendMessageDO = transToMessage(servicePackOrderPlanDO);
// 消息订阅
subscribeMessage(servicePackSendMessageDO,servicePackOrderPlanDO);
// 对plan数据库进行修改状态
servicePackOrderPlanDO.setPlanState(SendStateEnum.SEND.getCode());
servicePackOrderPlanDO.setUpdateTime(new Date());
servicePackOrderPlanDAO.updateServicepackOrderPlan(servicePackOrderPlanDO);
message_logger.info("服务包任务计划,修改计划状态成功,计划编号:" + servicePackOrderPlanDO.getPlanNo());
}
}
}
}
}).start();
}