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

This article explores eleven practical approaches for implementing delayed tasks in Java, covering native APIs like DelayQueue and Timer, thread‑pool executors, popular message‑queue solutions such as RocketMQ and RabbitMQ, as well as Redis‑based techniques, Redisson, Netty, Hutool, Quartz, and simple polling implementations.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
11 Ways to Implement Delayed Tasks in Java – From DelayQueue to Quartz

DelayQueue

DelayQueue is a JDK API that provides a delayed queue. The generic type must implement the Delayed interface, which extends Comparable. The getDelay method returns the remaining time before execution, and compareTo orders tasks by their delay.

Demo

@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);
    }
}

Task fields:

taskContent – the content of the delayed task

delayTime – delay in seconds

Test code creates a DelayQueue<SanYouTask>, starts a thread that takes tasks, and offers three tasks with delays of 5 s, 3 s and 8 s. The result shows successful execution of delayed tasks.

Implementation Principle

The offer method inserts a task and sorts it using compareTo. The take method retrieves the head element, checks getDelay, and waits until the delay expires before returning the task.

Timer

Timer is another JDK API for delayed execution.

Demo

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

Timer uses a TimerTask with a nextExecutionTime field to calculate the execution moment. Internally a single thread processes tasks, which can cause delays if a task runs long or throws an exception, so it is discouraged in modern practice.

ScheduledThreadPoolExecutor

Introduced in JDK 1.5, it solves Timer’s single‑thread and exception‑crash problems.

Demo

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

It uses a DelayedWorkQueue internally; tasks are wrapped as ScheduledFutureTask and ordered by their delay.

RocketMQ

RocketMQ provides delayed messages with 18 predefined delay levels. A producer sets the delay level on a Message, and the broker stores the message in a special schedule topic until the delay expires, then forwards it to the original topic.

Demo

@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("Submitting delayed task");
        producer.send(msg);
    }
}

A consumer annotated with @RocketMQMessageListener receives the message after the delay.

RabbitMQ

RabbitMQ can implement delayed tasks via dead‑letter queues. A normal queue is configured with a TTL (e.g., 5 s) and a dead‑letter exchange. When a message expires, it is routed to the dead‑letter exchange and then to a consumer queue.

Demo

@RestController
@Slf4j
public class RabbitMQDelayTaskController {
    @Resource
    private RabbitTemplate rabbitTemplate;
    @GetMapping("/rabbitmq/add")
    public void addTask(@RequestParam("task") String task) throws Exception {
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        log.info("Submitting delayed task");
        rabbitTemplate.convertAndSend("sanyouDirectExchangee", "", task, correlationData);
    }
}

The workflow: producer → direct exchange → queue with TTL → dead‑letter exchange → final queue → consumer.

Listening to Redis Expired Keys

Redis publishes an event on the channel __keyevent@<db>__:expired when a key expires. By storing the delayed task as a key with the desired TTL and listening to this channel, the application can react to the expiration.

Demo

@Configuration
public class RedisConfiguration {
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        return container;
    }
    @Bean
    public KeyExpirationEventMessageListener redisKeyExpirationListener(RedisMessageListenerContainer container) {
        return new KeyExpirationEventMessageListener(container);
    }
}

When an expiration event is received, a listener prints the key. However, this method has drawbacks: expiration events are only emitted after the key is actually removed (lazy or periodic eviction), messages are not persisted, and all expired keys are reported, requiring filtering.

Redisson RDelayedQueue

Redisson offers a distributed delayed queue built on Redis data structures. Tasks are stored in a sorted set ( redisson_delay_queue_timeout) with a score equal to the execution timestamp. A background thread moves due tasks to a list ( SANYOU) that consumers poll.

Demo

@Component
@Slf4j
public class RedissonDelayQueue {
    private RedissonClient redissonClient;
    private RDelayedQueue<String> delayQueue;
    private RBlockingQueue<String> blockingQueue;
    @PostConstruct
    public void init() {
        initDelayQueue();
        startDelayQueueConsumer();
    }
    private void initDelayQueue() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");
        redissonClient = Redisson.create(config);
        blockingQueue = redissonClient.getBlockingQueue("SANYOU");
        delayQueue = redissonClient.getDelayedQueue(blockingQueue);
    }
    private void startDelayQueueConsumer() {
        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("Adding delayed task:{} delay:{}s", task, seconds);
        delayQueue.offer(task, seconds, TimeUnit.SECONDS);
    }
}

A REST controller calls offerTask to add a 5‑second delayed job.

Netty HashedWheelTimer

Netty’s HashedWheelTimer implements a timing wheel. The wheel is divided into slots; each slot represents a time slice. Tasks are hashed to a slot based on their delay and executed when the wheel reaches that slot.

Demo

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

Hutool SystemTimer

Hutool provides SystemTimer, which internally uses a timing wheel similar to Netty.

Demo

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

Quartz Scheduler

Quartz is a full‑featured job‑scheduling framework. A Job implementation contains the task logic, a Trigger defines the execution time, and a Scheduler orchestrates them.

Demo

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("Submitting delayed task");
        scheduler.scheduleJob(job, trigger);
    }
}

Infinite Polling Delay Task

A simple approach creates a thread that continuously scans a list of tasks and executes those whose trigger time has passed.

Demo

@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 delayTask : DELAY_TASK_LIST) {
                        if (delayTask.triggerTime <= System.currentTimeMillis()) {
                            log.info("Processing delayed task:{}", delayTask.taskContent);
                            DELAY_TASK_LIST.remove(delayTask);
                        }
                    }
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (Exception e) {
                }
            }
        }).start();
        log.info("Submitting 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;
        }
    }
}

This method is easy but inefficient because it scans all tasks on each iteration.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Javaconcurrencydelayed tasks
Su San Talks Tech
Written by

Su San Talks Tech

Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.

0 followers
Reader feedback

How this landed with the community

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.