Timer 与 DelayQueue

本文探讨了两种实现邮件发送重试机制的方法:使用Timer与TimerTask组合进行定时重试,以及利用DelayQueue实现延迟队列来处理邮件重试。通过具体代码示例说明了各自的优缺点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介

有这样一个业务场景,很多业务需要发邮件,如果失败了要重试,每隔5分钟重试一次,最多12次,如果是12次都失败,就记入数据库。

粗一想,很简单嘛,但是仔细想一想,好像不是那么容易,在想一想,嗯,也不是那么难。还是要亲自试一下,写一下代码才知道有哪些坑。

分析

其实,问题的关键在于时间间隔的处理,是使用定时器,还是队列把时间封装一下。

注意,很容易把这2中情况混在一起了。

下面就来把这两种情况都实现一下。

Timer实现

很多对线程池比较熟悉的朋友可能首先想到的是ScheduledThreadPoolExecutor,但是ScheduledThreadPoolExecutor有一个问题就是没有办法取消。

比如发送到第5次成功了,就需要取消周期任务,避免重复发送,ScheduledThreadPoolExecutor是不好实现的。

所以我们直接使用Timer和TimerTask。

先来一个TimerTask

import java.util.TimerTask;

public class MailSendTimerTask extends TimerTask {

    private int exeNum;

    private String name;

    public MailSendTimerTask(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        if(exeNum < 12){
            try{
                if(MailUtil.sendMail(name)){
                    this.cancel();
                }
            }finally {
                exeNum++;
            }
        }else {
            MailUtil.logError(name);
            this.cancel();
        }
    }
}

如果发送成功或者到了12次都失败了,我们就取消任务。

MailUtil假装发了邮件和失败后记录到数据库了

import java.util.Random;

public class MailUtil {

    private static final Random random = new Random();

    private static final boolean [] deafult_boolean = {false,false,false,false,false,false,false,false,false,false,false,false,true};

    public static boolean sendMail(String name){
        System.out.println("send mail:" + name);
        return deafult_boolean[random.nextInt(deafult_boolean.length)];
    }

    public static void logError(String name){
        System.out.println("log error:" + name);
    }
}

来一个测试类:

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Start {

    /**
     * 5分钟
     */
//    public static final int PERIOD = 1000 * 60 * 5;
    /**
     * 2秒
      */
    public static final int PERIOD = 1000 * 2;

    private static final Timer timer = new Timer();

    private static final ScheduledThreadPoolExecutor scheduledExe = new ScheduledThreadPoolExecutor(2);

    public static void main(String[] args) {

        for(int i = 0 ;i<20;i++){
            addTask(new MailSendTimerTask(String.valueOf(i)));
//            exeTask(new MailSendTimerTask(String.valueOf(i)));
        }
    }

    public static void addTask(TimerTask task){
        timer.schedule(task,0, PERIOD);
    }

    public static void exeTask(TimerTask task){
        scheduledExe.scheduleAtFixedRate(task,0,PERIOD, TimeUnit.MILLISECONDS);
    }
}

我们可以看到使用ScheduledThreadPoolExecutor是没有办法取消任务的。

Timer有一些问题,首先Timer是单线程的,另外Timer执行任务抛出异常后,后面的任务就都不会执行了。

所以我们来看一下队列方式的实现。

DelayQueue实现

为了方便我们使用DelayQueue阻塞队列。

注意DelayQueue队列的元素需要实现Delayed。

public interface Delayed extends Comparable<Delayed> {

    /**
     * Returns the remaining delay associated with this object, in the
     * given time unit.
     *
     * @param unit the time unit
     * @return the remaining delay; zero or negative values indicate
     * that the delay has already elapsed
     */
    long getDelay(TimeUnit unit);
}

Delayed主要需要获取的是剩余的延迟时间和比较元素的优先级(继承了Comparable)

当getDelay<=0的时候才能从DelayQueue中获取到元素。

先来一个实现看一下:

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class Email implements Delayed{

    /**
     * 毫秒
     */
    public static final int PERIOD = 1000 * 1;

    private String title;

    private Integer retryTimes;

    private long lastSendTime;

    public Email(String title, Integer retryTimes, long lastSendTime) {
        this.title = title;
        this.retryTimes = retryTimes;
        this.lastSendTime = lastSendTime;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        long delay = lastSendTime + PERIOD - System.currentTimeMillis();
//        System.out.println(TimeUnit.SECONDS.convert(delay,TimeUnit.MILLISECONDS));
        return unit.convert(delay,TimeUnit.MILLISECONDS);
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public Integer getRetryTimes() {
        return retryTimes;
    }

    public void setRetryTimes(Integer retryTimes) {
        this.retryTimes = retryTimes;
    }

    public long getLastSendTime() {
        return lastSendTime;
    }

    public void setLastSendTime(long lastSendTime) {
        this.lastSendTime = lastSendTime;
    }

    @Override
    public int compareTo(Delayed o) {
        if(!(o instanceof Email)){
            return -1;
        }
        Email that = (Email)o;
        long diff = this.lastSendTime - that.lastSendTime;
        if(diff == 0)
            return 0;
        else if(diff > 0)
            return 1;
        else
            return -1;
    }

    @Override
    public String toString() {
        return "Email{" +
                "title='" + title + '\'' +
                ", retryTimes=" + retryTimes +
                ", lastSendTime=" + lastSendTime +
                '}';
    }
}

注意:getDelay必须小于等于0才能够从队列里面获取到,所以getDelay返回值应该是动态变化的,一般是和当前时间相关的。

getDelay的返回值必须的时间单位必须是纳秒。可以看一下DelayQueue的take中的实现

available.awaitNanos(delay);

这里简单说一下这个思路,我们知道DelayQueue中的队列可能有元素但是没有到延迟时间是取不到的。

如果没有元素,我们可以在加入元素的时候通知,但是有元素,时间没到怎么处理呢?

让while循环一直取?显然这是不好的,消耗cpu,DelayQueue解决方式是,使用延迟时间优先级队列,这样取出来的就是延迟时间最短的,然后再等待一个延迟时间。这样就可以在等待的时候让出cpu,避免无用的消耗。

compareTo也大(返回值>0),表示延迟时间也长,优先级也低。

我们有了Delayed就可以使用DelayQueue来存放任务了,然后就可以开线程池来执行任务了。

import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


public class MailHandler {

    private static final Random random = new Random();

    private static final DelayQueue<Email> failQueue = new DelayQueue<Email>();

    private static final ExecutorService service = Executors.newFixedThreadPool(2);

    private static final MailHandler instance = new MailHandler();

    private static final boolean [] deafult_boolean = {false,false,false,false,false,false,false,false,false,false,false,false,true};

    private MailHandler(){
        service.submit(new MailSender());
    }

    public static MailHandler getInstance(){
        return instance;
    }

    public void failHandle(List<Email> mails) {
        failQueue.addAll(mails);
    }

    private static class MailSender implements Runnable{
        @Override
        public void run() {
            while (true){
                Email to = null;
                try {
                    to = failQueue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if(to == null) {
                    continue;
                }
                System.out.println(to);
                if(!deafult_boolean[random.nextInt(deafult_boolean.length)]){
                    Integer retryTimes = to.getRetryTimes();
                    if(retryTimes < 3) {
                        to.setRetryTimes(retryTimes + 1);
                        to.setLastSendTime(System.currentTimeMillis());
                        failQueue.offer(to);
                    }else{
                        System.out.println(to.getTitle() + "----Failure!");
                    }
                }else{
                    System.out.println(to.getTitle() + "----Success!");
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MailHandler instance = MailHandler.getInstance();
        LinkedList<Email> list = new LinkedList<>();
        Random random = new Random();
        long lastSendTime = System.currentTimeMillis();
        for(int i =0;i<20;i++){
            list.add(new Email(String.valueOf(i),0, lastSendTime + random.nextInt(5) * 1000));
        }
        instance.failHandle(list);
    }
}

能看出上面的代码有那些问题吗?

其实这就是一个生成消费者模式,我们开了一个2个线程从线程池,但是我们只提交了一个任务。 所以我们可以稍微修改一下构造函数:

private MailHandler(){
    service.execute(new MailSender());
    service.execute(new MailSender());
}

这里使用Executors.newFixedThreadPool是在适合不过了,因为从开始我们就能决定使用多少个线程来处理任务,和不确定任务数明显不同。

你可以尝试重构一下,也许就能发现这其中的一些微妙的区别。亲自修改一下代码试一下,调试一下,是最容易发现问题的方式。

我们也没有使用submit的方式了,而是使用的execute方式,如果不关心结果的话最好使用execute,如果出错了至少能得到一点堆栈信息,如果使用submit就什么也没有了,除非自定义ThreadPoolExecutor处理了堆栈信息。

使用:

System.out.println(Thread.currentThread().getName()+ ":" + to);

代替:

System.out.println(to);

可以看一下是由哪一个线程处理的任务。

执行结果

转载于:https://my.oschina.net/u/2474629/blog/1919127

### Timer 延迟算法的实现优化方法 Timer 延迟算法通常涉及对定时任务的管理,其核心目标是高效地处理延时任务和周期性任务。以下是关于 Timer 延迟算法的实现优化方法的详细分析: #### 1. 时间轮(Timing Wheel)算法 时间轮是一种高效的定时器实现机制,特别适用于需要处理大量定时任务的场景[^2]。它通过将时间分片并使用环形数组来管理定时任务,极大地提高了定时器的性能和可扩展性。时间轮的设计可以有效地解决传统 `Timer` 和 `ScheduledThreadPoolExecutor` 的一些问题。 时间轮的基本原理包括以下几点: - 将时间划分为多个固定的时间段(例如秒、毫秒),每个时间段对应时间轮中的一个槽位。 - 每个槽位存储在该时间段内到期的任务。 - 使用一个指针按固定频率移动,检查当前槽位中是否有任务需要执行。 这种设计的优点在于,它避免了每次插入任务时都需要进行排序或查找操作,从而显著提升了性能。 #### 2. JDK 自带的 Timer 和 ScheduledThreadPoolExecutor 的局限性 JDK 提供的 `Timer` 和 `ScheduledThreadPoolExecutor` 是常用的定时任务管理工具,但它们存在一些局限性[^1]: - **精确度问题**:`Timer` 和 `ScheduledThreadPoolExecutor` 可能会因为线程调度或其他因素导致任务执行时间不准确。 - **高负载下的性能瓶颈**:当任务数量较多时,`Timer` 和 `ScheduledThreadPoolExecutor` 的性能可能会下降,尤其是在任务队列较长的情况下。 #### 3. 时间轮算法的优化 为了进一步提升时间轮算法的性能,可以考虑以下优化方法: - **多级时间轮**:对于长时间任务,可以引入多级时间轮的设计。第一级时间轮处理短时间任务,第二级时间轮处理长时间任务。这样可以减少大时间跨度任务对低级别时间轮的影响[^2]。 - **动态调整时间粒度**:根据任务的实际分布情况动态调整时间轮的时间粒度,以适应不同的应用场景。 - **任务批量处理**:对于同一时间段内的多个任务,可以采用批量处理的方式,减少任务切换的开销。 - **内存优化**:通过复用任务对象或使用更紧凑的数据结构来降低内存消耗。 #### 4. 其他延迟算法的优化方法 除了时间轮算法外,还可以结合其他技术对延迟算法进行优化: - **优先级队列**:使用优先级队列对任务进行排序,确保最早到期的任务优先被执行。这种方式适用于任务数量较少但对延迟要求较高的场景。 - **分片技术**:将任务按照某种规则(如哈希值)分配到不同的分片中,每个分片独立运行,从而提高并发处理能力。 - **异步执行**:通过异步编程模型(如 Reactor 模型或 Actor 模型)来处理延迟任务,减少线程阻塞带来的性能损耗。 ```python import heapq from threading import Thread, Event class DelayTask: def __init__(self, delay, task): self.delay = delay self.task = task self.trigger_time = time.time() + delay def __lt__(self, other): return self.trigger_time < other.trigger_time class DelayQueue: def __init__(self): self.queue = [] self.event = Event() def add_task(self, delay, task): heapq.heappush(self.queue, DelayTask(delay, task)) self.event.set() def run(self): while True: self.event.wait() if not self.queue: self.event.clear() continue now = time.time() while self.queue and self.queue[0].trigger_time <= now: task = heapq.heappop(self.queue).task task() if not self.queue: self.event.clear() ``` 上述代码展示了一个基于优先级队列的延迟任务队列实现。通过维护一个最小堆,可以高效地找到最早到期的任务。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值