Backend Development 27 min read

11 Ways to Implement Delayed Tasks in Java: From DelayQueue to Quartz

This article explores eleven practical methods for implementing delayed tasks in Java, covering native APIs like DelayQueue and Timer, advanced executors such as ScheduledThreadPoolExecutor, and popular messaging solutions including RocketMQ, RabbitMQ, Redis, Redisson, Netty, Hutool, and Quartz, with code demos and implementation details.

Sanyou's Java Diary
Sanyou's Java Diary
Sanyou's Java Diary
11 Ways to Implement Delayed Tasks in Java: From DelayQueue to Quartz

Hello, I'm SanYou.

Delayed tasks are common in daily life, such as order payment timeout cancellation or automatic receipt confirmation.

This article reviews 11 implementation methods for delayed tasks, each suitable for different scenarios.

DelayQueue

DelayQueue is a JDK-provided API representing a delayed queue.

The generic type must implement the Delayed interface, which extends Comparable .

The getDelay method returns the remaining time before execution; a negative value means the task is ready.

The compareTo method orders tasks so the earliest one is at the head of the queue.

Demo

<code>@Getter
public class SanYouTask implements Delayed {
    private final String taskContent;
    private final Long triggerTime;
    public SanYouTask(String taskContent, Long delayTime) {
        this.taskContent = taskContent;
        this.triggerTime = System.currentTimeMillis() + delayTime * 1000;
    }
    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(triggerTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }
    @Override
    public int compareTo(Delayed o) {
        return this.triggerTime.compareTo(((SanYouTask) o).triggerTime);
    }
}
</code>

SanYouTask implements Delayed . Constructor parameters:

taskContent: the content of the delayed task

delayTime: delay in seconds

Test

<code>@Slf4j
public class DelayQueueDemo {
    public static void main(String[] args) {
        DelayQueue<SanYouTask> queue = new DelayQueue<>();
        new Thread(() -> {
            while (true) {
                try {
                    SanYouTask task = queue.take();
                    log.info("Got delayed task:{}", task.getTaskContent());
                } catch (Exception e) {}
            }
        }).start();
        log.info("Submit delayed tasks");
        queue.offer(new SanYouTask("SanYou Java diary 5s", 5L));
        queue.offer(new SanYouTask("SanYou Java diary 3s", 3L));
        queue.offer(new SanYouTask("SanYou Java diary 8s", 8L));
    }
}
</code>

A thread takes tasks from the queue; three tasks with delays of 5 s, 3 s, and 8 s are submitted and executed successfully.

Implementation Principle

The offer method sorts tasks using compareTo , placing the earliest task at the queue head. The take method retrieves the head element, checks getDelay , and waits if the task is not yet ready.

Timer

Timer is another JDK API for delayed execution.

Demo

<code>@Slf4j
public class TimerDemo {
    public static void main(String[] args) {
        Timer timer = new Timer();
        log.info("Submit delayed task");
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                log.info("Execute delayed task");
            }
        }, 5000);
    }
}
</code>

Timer schedules a TimerTask with a 5‑second delay.

Implementation Principle

TimerTask holds a nextExecutionTime field. Timer maintains a TaskQueue sorted by this timestamp and a dedicated thread that executes tasks when their time arrives.

Although convenient, Timer is discouraged in Alibaba’s guidelines because it uses a single thread and lacks exception handling.

ScheduledThreadPoolExecutor

Introduced in JDK 1.5 to address Timer’s shortcomings, it provides a thread pool and a DelayedWorkQueue for scheduling.

Demo

<code>@Slf4j
public class ScheduledThreadPoolExecutorDemo {
    public static void main(String[] args) {
        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2, new ThreadPoolExecutor.CallerRunsPolicy());
        log.info("Submit delayed task");
        executor.schedule(() -> log.info("Execute delayed task"), 5, TimeUnit.SECONDS);
    }
}
</code>

Tasks are wrapped into ScheduledFutureTask objects and placed in the DelayedWorkQueue , which orders them by delay.

RocketMQ

RocketMQ provides delayed messages with 18 predefined delay levels. A producer sets the delay level when sending a message.

Demo

<code>@RestController
@Slf4j
public class RocketMQDelayTaskController {
    @Resource
    private DefaultMQProducer producer;
    @GetMapping("/rocketmq/add")
    public void addTask(@RequestParam("task") String task) throws Exception {
        Message msg = new Message("sanyouDelayTaskTopic", "TagA", task.getBytes(RemotingHelper.DEFAULT_CHARSET));
        msg.setDelayTimeLevel(2); // 5 seconds
        log.info("Submit delayed task");
        producer.send(msg);
    }
}
</code>

The consumer listens on the same topic and logs received delayed tasks.

Implementation Principle

RocketMQ stores delayed messages in a special SCHEDULE_TOPIC_XXXX and a background thread moves them to the real topic when the delay expires, ensuring persistence.

RabbitMQ

RabbitMQ can achieve delayed tasks via dead‑letter queues.

Demo

<code>@Configuration
public class RabbitMQConfiguration {
    @Bean
    public DirectExchange sanyouDirectExchangee() { return new DirectExchange("sanyouDirectExchangee"); }
    @Bean
    public Queue sanyouQueue() { return QueueBuilder.durable("sanyouQueue").ttl(5000).deadLetterExchange("sanyouDelayTaskExchangee").build(); }
    @Bean
    public Binding sanyouQueueBinding() { return BindingBuilder.bind(sanyouQueue()).to(sanyouDirectExchangee()).with(""); }
    @Bean
    public DirectExchange sanyouDelayTaskExchange() { return new DirectExchange("sanyouDelayTaskExchangee"); }
    @Bean
    public Queue sanyouDelayTaskQueue() { return QueueBuilder.durable("sanyouDelayTaskQueue").build(); }
    @Bean
    public Binding sanyouDelayTaskQueueBinding() { return BindingBuilder.bind(sanyouDelayTaskQueue()).to(sanyouDelayTaskExchange()).with(""); }
}
</code>
<code>@RestController
@Slf4j
public class RabbitMQDelayTaskController {
    @Resource
    private RabbitTemplate rabbitTemplate;
    @GetMapping("/rabbitmq/add")
    public void addTask(@RequestParam("task") String task) throws Exception {
        CorrelationData cd = new CorrelationData(UUID.randomUUID().toString());
        log.info("Submit delayed task");
        rabbitTemplate.convertAndSend("sanyouDirectExchangee", "", task, cd);
    }
}
</code>

The message expires after 5 s, moves to the dead‑letter exchange, and is finally consumed.

Implementation Principle

Messages first land in a TTL queue; upon expiration they are routed to a dead‑letter exchange and then to the final consumer queue.

Listening to Redis Expired Keys

Redis publishes an event on the __keyevent@<db>__:expired channel when a key expires.

Demo

<code>set sanyou task
expire sanyou 5
</code>

A listener receives the expired key and processes the delayed task.

Limitations

Expiration events are emitted only after the key is actually removed, which may be delayed by lazy or periodic eviction.

Redis Pub/Sub has no persistence; messages can be lost if no subscriber is present.

All subscribers receive every expired key, requiring filtering logic.

Redisson RDelayedQueue

Redisson builds a delayed queue on top of Redis data structures.

Demo

<code>@Component
@Slf4j
public class RedissonDelayQueue {
    private RedissonClient redissonClient;
    private RDelayedQueue<String> delayQueue;
    private RBlockingQueue<String> blockingQueue;
    @PostConstruct
    public void init() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");
        redissonClient = Redisson.create(config);
        blockingQueue = redissonClient.getBlockingQueue("SANYOU");
        delayQueue = redissonClient.getDelayedQueue(blockingQueue);
        startConsumer();
    }
    private void startConsumer() {
        new Thread(() -> {
            while (true) {
                try {
                    String task = blockingQueue.take();
                    log.info("Received delayed task:{}", task);
                } catch (Exception e) { e.printStackTrace(); }
            }
        }, "SANYOU-Consumer").start();
    }
    public void offerTask(String task, long seconds) {
        log.info("Add delayed task:{} delay:{}s", task, seconds);
        delayQueue.offer(task, seconds, TimeUnit.SECONDS);
    }
}
</code>
<code>@RestController
public class RedissonDelayQueueController {
    @Resource
    private RedissonDelayQueue redissonDelayQueue;
    @GetMapping("/add")
    public void addTask(@RequestParam("task") String task) {
        redissonDelayQueue.offerTask(task, 5);
    }
}
</code>

Implementation Principle

Redisson uses a sorted set redisson_delay_queue_timeout:SANYOU to store tasks with timestamps, a list SANYOU as the final queue, and a channel to trigger moving ready tasks from the sorted set to the list.

Netty HashedWheelTimer

Demo

<code>@Slf4j
public class NettyHashedWheelTimerDemo {
    public static void main(String[] args) {
        HashedWheelTimer timer = new HashedWheelTimer(100, TimeUnit.MILLISECONDS, 8);
        timer.start();
        log.info("Submit delayed task");
        timer.newTimeout(t -> log.info("Execute delayed task"), 5, TimeUnit.SECONDS);
    }
}
</code>

Implementation Principle

The timer divides time into slots (8 slots of 100 ms each). Tasks are hashed into slots based on their expiration time; a worker thread scans slots and executes due tasks.

Hutool SystemTimer

Demo

<code>@Slf4j
public class SystemTimerDemo {
    public static void main(String[] args) {
        SystemTimer systemTimer = new SystemTimer();
        systemTimer.start();
        log.info("Submit delayed task");
        systemTimer.addTask(new TimerTask(() -> log.info("Execute delayed task"), 5000));
    }
}
</code>

Hutool internally also uses a time‑wheel mechanism.

Quartz Scheduler

Demo

<code>public class QuartzDemo {
    public static void main(String[] args) throws SchedulerException {
        SchedulerFactory sf = new StdSchedulerFactory();
        Scheduler scheduler = sf.getScheduler();
        scheduler.start();
        JobDetail job = JobBuilder.newJob(SanYouJob.class)
            .usingJobData("delayTask", "This is a delayed task")
            .build();
        Trigger trigger = TriggerBuilder.newTrigger()
            .startAt(DateUtil.offsetSecond(new Date(), 5))
            .build();
        log.info("Submit delayed task");
        scheduler.scheduleJob(job, trigger);
    }
}
</code>
<code>@Slf4j
public class SanYouJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobDataMap map = context.getJobDetail().getJobDataMap();
        log.info("Got delayed task:{}", map.getString("delayTask"));
    }
}
</code>

Implementation Principle

Quartz uses Job , JobDetail , Trigger , and Scheduler . A dedicated thread checks triggers and dispatches jobs to a thread pool when their fire time arrives.

Infinite Polling Delay Task

Demo

<code>@Slf4j
public class PollingTaskDemo {
    private static final List<DelayTask> DELAY_TASK_LIST = new CopyOnWriteArrayList<>();
    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                try {
                    for (DelayTask dt : DELAY_TASK_LIST) {
                        if (dt.triggerTime <= System.currentTimeMillis()) {
                            log.info("Process delayed task:{}", dt.taskContent);
                            DELAY_TASK_LIST.remove(dt);
                        }
                    }
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (Exception e) {}
            }
        }).start();
        log.info("Submit delayed task");
        DELAY_TASK_LIST.add(new DelayTask("SanYou Java diary", 5L));
    }
    @Getter @Setter
    public static class DelayTask {
        private final String taskContent;
        private final Long triggerTime;
        public DelayTask(String taskContent, Long delayTime) {
            this.taskContent = taskContent;
            this.triggerTime = System.currentTimeMillis() + delayTime * 1000;
        }
    }
}
</code>

This simple approach scans all tasks periodically, which is easy but inefficient.

Conclusion

All example code is available at https://github.com/sanyou3/delay-task-demo.git .

JavaConcurrencyRedisMessage Queuedelayed tasks
Sanyou's Java Diary
Written by

Sanyou's Java Diary

Passionate about technology, though not great at solving problems; eager to share, never tire of learning!

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.