java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java定时任务执行

Java中实现定时任务执行的几种常见方式总结

作者:inrgihc

几乎在所有的项目中,定时任务的使用都是不可或缺的,如果使用不当甚至会造成资损,这篇文章主要总结介绍了Java中实现定时任务执行的几种常见方式,需要的朋友可以参考下

一、概述

关于在Java项目中实现定时任务执行的几种方式总结如下:

单进程实现定时任务主要有以下几种方式:

1. Timer 和 TimerTask(JDK 原生)

2. ScheduledExecutorService(JDK 1.5+,推荐)

3. Spring Task(Spring 框架支持)

4. Quartz(企业级任务调度框架)

5. DelayQueue 阻塞队列

分布式定时任务

1、使用Redis来实现定时任务

2、使用xxl-job实现定时任务

二、详情

1. Timer 和 TimerTask(JDK 原生)

Java中的Timer类是一个定时调度器,用于在指定的时间点执行任务。Timer 类用于实现定时任务,最大的好处就是他的实现非常简单,特别的轻量级,因为它是Java内置的,所以只需要简单调用就行了。

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);
    }
}
优点缺点
  • 简单易用,适合单线程任务调度。
  • 基于绝对时间(Date)执行任务。
  • Timer内部是单线程执行任务的,如果某个任务执行时间较长,会影响后续任务的执行
  • 如果任务抛出未捕获异常,将导致整个 Timer 线程终止,影响其他任务的执行
  • Timer 无法提供高精度的定时任务。因为系统调度和任务执行时间的不确定性,可能导致任务执行的时间不准确。
  • 虽然可以使用 cancel 方法取消任务,但这仅仅是将任务标记为取消状态,仍然会在任务队列中占用位置,无法释放资源。这可能导致内存泄漏。
  • 当有大量任务时,Timer 的性能可能受到影响,因为它在每次扫描任务队列时都要进行时间比较。
  • Timer执行任务完全基于JVM内存,一旦应用重启,那么队列中的任务就都没有了

2. ScheduledExecutorService(JDK 1.5+,推荐)

ScheduledExecutorService基于线程池(ThreadPoolExecutor),支持多任务并发执行。相比 Timer,更稳定、更灵活。

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);

// 延迟 1s 后执行
scheduler.schedule(() -> {
    System.out.println("Task executed once at: " + new Date());
}, 1, TimeUnit.SECONDS);

// 延迟 1s 后执行,之后每 2s 执行一次
scheduler.scheduleAtFixedRate(() -> {
    System.out.println("Fixed-rate task executed at: " + new Date());
}, 1, 2, TimeUnit.SECONDS);
优点缺点
  • 是 JDK 1.5 之后自带的 API,因此使用起来也比较方便
  • 执行任务时使用线程池管理,不会造成任务间的相互影响
  • 支持 scheduleAtFixedRate(固定频率)和 scheduleWithFixedDelay(固定间隔)。
  • 需要配置线程池
  • 如果任务抛出异常,后续任务 会被取消!需在任务内捕获异常。

3. Spring Task(Spring 框架支持)

适合 Spring/Spring Boot 项目,基于 ScheduledExecutorService,提供注解式任务调度。

@EnableScheduling
@SpringBootApplication
public class SpringbootApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(SpringbootApplication.class, args);
    }
}

@Component
public class TimerTask {
 
    @Scheduled(cron = "0 0 0 0 7 *")
    public void task() {
        System.out.println("定时任务...");
    }
}
优点缺点
  • 注解驱动,开发简单。
  • 支持 Cron 表达式(如 0 0 12 * * ? 表示每天中午 12 点执行)。
只适合 Spring/Spring Boot 项目

4. Quartz(企业级任务调度框架)

Quartz是OpenSymphony开源组织在Job scheduling领域的一个Java实现的开源项目。

// 1. 定义 Job
public class MyJob implements Job {
    @Override
    public void execute(JobExecutionContext context) {
        System.out.println("Quartz Job executed at: " + new Date());
    }
}

// 2. 配置 Trigger 和 Scheduler
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();

JobDetail job = JobBuilder.newJob(MyJob.class)
    .withIdentity("myJob", "group1")
    .build();

Trigger trigger = TriggerBuilder.newTrigger()
    .withIdentity("myTrigger", "group1")
    .startNow()
    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
        .withIntervalInSeconds(5) // 每 5s 执行一次
        .repeatForever())
    .build();

scheduler.scheduleJob(job, trigger);
scheduler.start();
优点缺点
  • 支持数据库持久化,重启后可恢复调度
  • 支持动态调整任务(如暂停、恢复、修改执行时间)
  • 分布式需要使用数据库

5. DelayQueue 阻塞队列

DelayQueue是一个带有延迟时间的无界阻塞队列,它的元素必须实现Delayed接口。当从DelayQueue中取出一个元素时,如果其延迟时间还未到达,则会阻塞等待,直到延迟时间到达。因此,我们可以通过将任务封装成实现Delayed接口的元素,将其放入DelayQueue中,再使用一个线程不断地从DelayQueue中取出元素并执行任务,从而实现定时任务的调度。

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

public class DelayedTask implements Delayed {
    private final String taskName;
    private final long executeTime; // 执行时间(毫秒时间戳)

    public DelayedTask(String taskName, long delayMs) {
        this.taskName = taskName;
        this.executeTime = System.currentTimeMillis() + delayMs;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        // 返回剩余延迟时间
        long remainingDelay = executeTime - System.currentTimeMillis();
        return unit.convert(remainingDelay, TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed other) {
        // 按执行时间排序(早到期者优先)
        return Long.compare(this.executeTime, ((DelayedTask) other).executeTime);
    }

    public void execute() {
        System.out.println("Executing task: " + taskName + " at " + new Date(executeTime));
    }
}

public class DelayQueueScheduler {
    private static final DelayQueue<DelayedTask> queue = new DelayQueue<>();

    public static void main(String[] args) throws InterruptedException {
        // 提交延迟任务
        queue.put(new DelayedTask("Task 1", 3000)); // 3s 后执行
        queue.put(new DelayedTask("Task 2", 1000)); // 1s 后执行
        queue.put(new DelayedTask("Task 3", 5000)); // 5s 后执行

        // 启动消费者线程处理任务
        while (!queue.isEmpty()) {
            DelayedTask task = queue.take(); // 阻塞直到有任务到期
            task.execute();
        }
    }
}
优点缺点
  • 精确延迟、无第三方依赖
  • 只能单进程内使用

6、使用 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);
    }
}

7、使用xxl-job实现定时任务

xxl-job是一款分布式定时任务调度平台,可以实现各种类型的定时任务调度,如定时执行Java代码、调用HTTP接口、执行Shell脚本等。xxl-job采用分布式架构,支持集群部署,可以满足高并发、大数据量的任务调度需求。

三、开源项目

1、https://gitee.com/xuxueli0323/xxl-job

2、https://gitee.com/dromara/disjob

3、https://gitee.com/KFCFans/PowerJob

4、https://gitee.com/aizuda/snail-job

5、https://gitee.com/elasticjob/elastic-job

到此这篇关于Java中实现定时任务执行的几种常见方式总结的文章就介绍到这了,更多相关Java定时任务执行内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文