关于java:定时任务最简单的-3-种实现方法超实用

31次阅读

共计 8224 个字符,预计需要花费 21 分钟才能阅读完成。

定时工作在理论的开发中特地常见,比方电商平台 30 分钟后主动勾销未领取的订单,以及凌晨的数据汇总和备份等,都须要借助定时工作来实现,那么咱们本文就来看一下 定时工作最简略的几种实现形式。

TOP 1:Timer

Timer 是 JDK 自带的定时工作执行类,无论任何我的项目都能够间接应用 Timer 来实现定时工作,所以 Timer 的长处就是使用方便,它的实现代码如下:

public class MyTimerTask {public static void main(String[] args) {
        // 定义一个工作
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {System.out.println("Run timerTask:" + new Date());
            }
        };
        // 计时器
        Timer timer = new Timer();
        // 增加执行工作(提早 1s 执行,每 3s 执行一次)timer.schedule(timerTask, 1000, 3000);
    }
}

程序执行后果如下:

Run timerTask:Mon Aug 17 21:29:25 CST 2020

Run timerTask:Mon Aug 17 21:29:28 CST 2020

Run timerTask:Mon Aug 17 21:29:31 CST 2020

Timer 毛病剖析

Timer 类实现定时工作尽管不便,但在应用时须要留神以下问题。

问题 1:工作执行工夫长影响其余工作

当一个工作的执行工夫过长时,会影响其余工作的调度,如下代码所示:

public class MyTimerTask {public static void main(String[] args) {
        // 定义工作 1
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {System.out.println("进入 timerTask 1:" + new Date());
                try {
                    // 休眠 5 秒
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {e.printStackTrace();
                }
                System.out.println("Run timerTask 1:" + new Date());
            }
        };
        // 定义工作 2
        TimerTask timerTask2 = new TimerTask() {
            @Override
            public void run() {System.out.println("Run timerTask 2:" + new Date());
            }
        };
        // 计时器
        Timer timer = new Timer();
        // 增加执行工作(提早 1s 执行,每 3s 执行一次)timer.schedule(timerTask, 1000, 3000);
        timer.schedule(timerTask2, 1000, 3000);
    }
}

程序执行后果如下:

进入 timerTask 1:Mon Aug 17 21:44:08 CST 2020

Run timerTask 1:Mon Aug 17 21:44:13 CST 2020

Run timerTask 2:Mon Aug 17 21:44:13 CST 2020

进入 timerTask 1:Mon Aug 17 21:44:13 CST 2020

Run timerTask 1:Mon Aug 17 21:44:18 CST 2020

进入 timerTask 1:Mon Aug 17 21:44:18 CST 2020

Run timerTask 1:Mon Aug 17 21:44:23 CST 2020

Run timerTask 2:Mon Aug 17 21:44:23 CST 2020

进入 timerTask 1:Mon Aug 17 21:44:23 CST 2020

从上述后果中能够看出,当工作 1 运行工夫超过设定的间隔时间时,工作 2 也会提早执行。本来工作 1 和工作 2 的执行工夫距离都是 3s,但因为工作 1 执行了 5s,因而工作 2 的执行工夫距离也变成了 10s(和原定工夫不符)。

问题 2:工作异样影响其余工作

应用 Timer 类实现定时工作时,当一个工作抛出异样,其余工作也会终止运行,如下代码所示:

public class MyTimerTask {public static void main(String[] args) {
        // 定义工作 1
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {System.out.println("进入 timerTask 1:" + new Date());
                // 模仿异样
                int num = 8 / 0;
                System.out.println("Run timerTask 1:" + new Date());
            }
        };
        // 定义工作 2
        TimerTask timerTask2 = new TimerTask() {
            @Override
            public void run() {System.out.println("Run timerTask 2:" + new Date());
            }
        };
        // 计时器
        Timer timer = new Timer();
        // 增加执行工作(提早 1s 执行,每 3s 执行一次)timer.schedule(timerTask, 1000, 3000);
        timer.schedule(timerTask2, 1000, 3000);
    }
}

程序执行后果如下:

进入 timerTask 1:Mon Aug 17 22:02:37 CST 2020

Exception in thread “Timer-0” java.lang.ArithmeticException: / by zero

​ at com.example.MyTimerTask$1.run(MyTimerTask.java:21)

​ at java.util.TimerThread.mainLoop(Timer.java:555)

​ at java.util.TimerThread.run(Timer.java:505)

Process finished with exit code 0

Timer 小结

Timer 类实现定时工作的长处是不便,因为它是 JDK 自定的定时工作,但毛病是工作如果执行工夫太长或者是工作执行异样,会影响其余任务调度,所以在生产环境下倡议审慎应用。

TOP 2:ScheduledExecutorService

ScheduledExecutorService 也是 JDK 1.5 自带的 API,咱们能够应用它来实现定时工作的性能,也就是说 ScheduledExecutorService 能够实现 Timer 类具备的所有性能,并且它能够解决了 Timer 类存在的所有问题

ScheduledExecutorService 实现定时工作的代码示例如下:

public class MyScheduledExecutorService {public static void main(String[] args) {
        // 创立工作队列
        ScheduledExecutorService scheduledExecutorService =
                Executors.newScheduledThreadPool(10); // 10 为线程数量
        // 执行工作
        scheduledExecutorService.scheduleAtFixedRate(() -> {System.out.println("Run Schedule:" + new Date());
        }, 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次
    }
}

程序执行后果如下:

Run Schedule:Mon Aug 17 21:44:23 CST 2020

Run Schedule:Mon Aug 17 21:44:26 CST 2020

Run Schedule:Mon Aug 17 21:44:29 CST 2020

ScheduledExecutorService 可靠性测试

① 工作超时执行测试

ScheduledExecutorService 能够解决 Timer 工作之间相应影响的毛病,首先咱们来测试一个工作执行工夫过长,会不会对其余工作造成影响,测试代码如下:

public class MyScheduledExecutorService {public static void main(String[] args) {
        // 创立工作队列
        ScheduledExecutorService scheduledExecutorService =
                Executors.newScheduledThreadPool(10);
        // 执行工作 1
        scheduledExecutorService.scheduleAtFixedRate(() -> {System.out.println("进入 Schedule:" + new Date());
            try {
                // 休眠 5 秒
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {e.printStackTrace();
            }
            System.out.println("Run Schedule:" + new Date());
        }, 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次
        // 执行工作 2
        scheduledExecutorService.scheduleAtFixedRate(() -> {System.out.println("Run Schedule2:" + new Date());
        }, 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次
    }
}

程序执行后果如下:

Run Schedule2:Mon Aug 17 11:27:55 CST 2020

进入 Schedule:Mon Aug 17 11:27:55 CST 2020

Run Schedule2:Mon Aug 17 11:27:58 CST 2020

Run Schedule:Mon Aug 17 11:28:00 CST 2020

进入 Schedule:Mon Aug 17 11:28:00 CST 2020

Run Schedule2:Mon Aug 17 11:28:01 CST 2020

Run Schedule2:Mon Aug 17 11:28:04 CST 2020

从上述后果能够看出,当工作 1 执行工夫 5s 超过了执行频率 3s 时,并没有影响工作 2 的失常执行,因而应用 ScheduledExecutorService 能够防止工作执行工夫过长对其余工作造成的影响

② 工作异样测试

接下来咱们来测试一下 ScheduledExecutorService 在一个工作异样时,是否会对其余工作造成影响,测试代码如下:

public class MyScheduledExecutorService {public static void main(String[] args) {
        // 创立工作队列
        ScheduledExecutorService scheduledExecutorService =
                Executors.newScheduledThreadPool(10);
        // 执行工作 1
        scheduledExecutorService.scheduleAtFixedRate(() -> {System.out.println("进入 Schedule:" + new Date());
            // 模仿异样
            int num = 8 / 0;
            System.out.println("Run Schedule:" + new Date());
        }, 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次
        // 执行工作 2
        scheduledExecutorService.scheduleAtFixedRate(() -> {System.out.println("Run Schedule2:" + new Date());
        }, 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次
    }
}

程序执行后果如下:

进入 Schedule:Mon Aug 17 22:17:37 CST 2020

Run Schedule2:Mon Aug 17 22:17:37 CST 2020

Run Schedule2:Mon Aug 17 22:17:40 CST 2020

Run Schedule2:Mon Aug 17 22:17:43 CST 2020

从上述后果能够看出,当工作 1 出现异常时,并不会影响工作 2 的执行

ScheduledExecutorService 小结

在单机生产环境下倡议应用 ScheduledExecutorService 来执行定时工作,它是 JDK 1.5 之后自带的 API,因而应用起来也比拟不便,并且应用 ScheduledExecutorService 来执行工作,不会造成工作间的相互影响。

TOP 3:Spring Task

如果应用的是 Spring 或 Spring Boot 框架,能够间接应用 Spring Framework 自带的定时工作,应用下面两种定时工作的实现形式,很难实现设定了具体工夫的定时工作,比方当咱们须要每周五来执行某项工作时,但如果应用 Spring Task 就可轻松的实现此需要。

以 Spring Boot 为例,实现定时工作只需两步:

  1. 开启定时工作;
  2. 增加定时工作。

具体实现步骤如下。

① 开启定时工作

开启定时工作只须要在 Spring Boot 的启动类上申明 @EnableScheduling 即可,实现代码如下:

@SpringBootApplication
@EnableScheduling // 开启定时工作
public class DemoApplication {// do someing}

② 增加定时工作

定时工作的增加只须要应用 @Scheduled 注解标注即可,如果有多个定时工作能够创立多个 @Scheduled 注解标注的办法,示例代码如下:

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component // 把此类托管给 Spring,不能省略
public class TaskUtils {
    // 增加定时工作
    @Scheduled(cron = "59 59 23 0 0 5") // cron 表达式,每周五 23:59:59 执行
    public void doTask(){System.out.println("我是定时工作~");
    }
}

留神:定时工作是主动触发的无需手动干涉,也就是说 Spring Boot 启动后会主动加载并执行定时工作。

Cron 表达式

Spring Task 的实现须要应用 cron 表达式来申明执行的频率和规定,cron 表达式是由 6 位或者 7 位组成的(最初一位能够省略),每位之间以空格分隔,每位从左到右代表的含意如下:

其中 * 和 ? 号都示意匹配所有的工夫。

cron 表达式在线生成地址:https://cron.qqe2.com/

常识扩大:分布式定时工作

下面的办法都是对于单机定时工作的实现,如果是分布式环境能够应用 Redis 来实现定时工作。

应用 Redis 实现提早工作的办法大体可分为两类:通过 ZSet 的形式和键空间告诉的形式

① ZSet 实现形式

通过 ZSet 实现定时工作的思路是,将定时工作寄存到 ZSet 汇合中,并且将过期工夫存储到 ZSet 的 Score 字段中,而后通过一个无线循环来判断以后工夫内是否有须要执行的定时工作,如果有则进行执行,具体实现代码如下:

import redis.clients.jedis.Jedis;
import utils.JedisUtils;
import java.time.Instant;
import java.util.Set;

public class DelayQueueExample {
    // zset key
    private static final String _KEY = "myTaskQueue";
    
    public static void main(String[] args) throws InterruptedException {Jedis jedis = JedisUtils.getJedis();
        // 30s 后执行
        long delayTime = Instant.now().plusSeconds(30).getEpochSecond();
        jedis.zadd(_KEY, delayTime, "order_1");
        // 持续增加测试数据
        jedis.zadd(_KEY, Instant.now().plusSeconds(2).getEpochSecond(), "order_2");
        jedis.zadd(_KEY, Instant.now().plusSeconds(2).getEpochSecond(), "order_3");
        jedis.zadd(_KEY, Instant.now().plusSeconds(7).getEpochSecond(), "order_4");
        jedis.zadd(_KEY, Instant.now().plusSeconds(10).getEpochSecond(), "order_5");
        // 开启定时工作队列
        doDelayQueue(jedis);
    }

    /**
     * 定时工作队列生产
     * @param jedis Redis 客户端
     */
    public static void doDelayQueue(Jedis jedis) throws InterruptedException {while (true) {
            // 以后工夫
            Instant nowInstant = Instant.now();
            long lastSecond = nowInstant.plusSeconds(-1).getEpochSecond(); // 上一秒工夫
            long nowSecond = nowInstant.getEpochSecond();
            // 查问以后工夫的所有工作
            Set<String> data = jedis.zrangeByScore(_KEY, lastSecond, nowSecond);
            for (String item : data) {
                // 生产工作
                System.out.println("生产:" + item);
            }
            // 删除曾经执行的工作
            jedis.zremrangeByScore(_KEY, lastSecond, nowSecond);
            Thread.sleep(1000); // 每秒查问一次
        }
    }
}

② 键空间告诉

咱们能够通过 Redis 的键空间告诉来实现定时工作,它的实现思路是给所有的定时工作设置一个过期工夫,等到了过期之后,咱们通过订阅过期音讯就能感知到定时工作须要被执行了,此时咱们执行定时工作即可。

默认状况下 Redis 是不开启键空间告诉的,须要咱们通过 config set notify-keyspace-events Ex 的命令手动开启,开启之后定时工作的代码如下:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;
import utils.JedisUtils;

public class TaskExample {
    public static final String _TOPIC = "__keyevent@0__:expired"; // 订阅频道名称
    public static void main(String[] args) {Jedis jedis = JedisUtils.getJedis();
        // 执行定时工作
        doTask(jedis);
    }

    /**
     * 订阅过期音讯,执行定时工作
     * @param jedis Redis 客户端
     */
    public static void doTask(Jedis jedis) {
        // 订阅过期音讯
        jedis.psubscribe(new JedisPubSub() {
            @Override
            public void onPMessage(String pattern, String channel, String message) {
                // 接管到音讯,执行定时工作
                System.out.println("收到音讯:" + message);
            }
        }, _TOPIC);
    }
}

更多对于定时工作的实现,请点击《史上最全的提早工作实现形式汇总!附代码》。

关注公众号「Java 中文社群」发送“面试”,支付我整顿的最新面试复习资料。

正文完
 0