11 Ways to Implement Delayed Tasks – 16k Words, 28 Diagrams

This article surveys eleven Java‑based delayed‑task solutions—including DelayQueue, Timer, ScheduledThreadPoolExecutor, RocketMQ, RabbitMQ, Redis key‑expiration, Redisson, Netty’s HashedWheelTimer, Hutool SystemTimer, Quartz, and a simple polling loop—providing code demos, implementation principles, and practical pros and cons for each approach.

Shepherd Advanced Notes
Shepherd Advanced Notes
Shepherd Advanced Notes
11 Ways to Implement Delayed Tasks – 16k Words, 28 Diagrams

Delayed tasks such as order‑cancellation timeouts or automatic receipt confirmations are common. This article compares eleven implementation approaches, each with runnable demos, underlying principles, and suitability notes.

DelayQueue

JDK DelayQueue stores elements implementing Delayed. getDelay returns remaining time; compareTo orders tasks so the earliest expires first.

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

Demo submits three tasks with delays of 5 s, 3 s, and 8 s. A consumer thread calls take() to retrieve the head; when getDelay ≤ 0 the task is executed.

Timer

JDK Timer schedules a TimerTask via schedule.

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

Each TimerTask holds nextExecutionTime. Internally a single‑threaded TimerThread processes tasks from a TaskQueue sorted by nextExecutionTime. Alibaba guidelines discourage Timer because it runs on a single thread and does not catch runtime exceptions, which can crash the timer.

ScheduledThreadPoolExecutor

Introduced in JDK 1.5, it solves Timer shortcomings by using a thread pool. Internally it employs a DelayedWorkQueue and wraps tasks as ScheduledFutureTask.

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

Multiple threads can execute tasks, and the queue orders them by delay.

RocketMQ

RocketMQ provides delayed messages with 18 predefined delay levels. A producer sets the delay level; the broker stores the message in a special topic SCHEDULE_TOPIC_XXXX until the delay expires, then moves it to the original topic.

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

A consumer listens on sanyouDelayTaskTopic and receives the message after the delay. Because RocketMQ persists messages, delayed tasks survive server restarts.

RabbitMQ

RabbitMQ implements delayed tasks via dead‑letter queues with TTL. The original queue has a 5 s TTL; expired messages are routed to a dead‑letter exchange and finally to a delay‑task queue.

@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("Submit delayed task");
        rabbitTemplate.convertAndSend("sanyouDirectExchangee", "", task, correlationData);
    }
}

Workflow: message → direct exchange → original queue (TTL 5 s) → dead‑letter exchange → delay‑task queue → consumer. RabbitMQ does not persist dead‑lettered messages, so loss is possible.

Redis Key Expiration

Redis publishes an event on channel __keyevent@<db>__:expired when a key is removed after expiration. By storing the delayed task as a key with TTL and listening to this channel, the task can be retrieved.

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

Drawbacks: expiration events are emitted only after the key is actually removed (lazy or periodic eviction), there is no persistence, and delivery is broadcast‑only.

Redisson RDelayedQueue

Redisson builds a delayed queue on top of Redis. It uses a sorted set redisson_delay_queue_timeout:SANYOU to store tasks with scores = trigger timestamp, a list SANYOU as the target queue, and a channel redisson_delay_queue_channel:SANYOU to trigger movement of ready tasks.

@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("Add delayed task:{} delay:{}s", task, seconds);
        delayQueue.offer(task, seconds, TimeUnit.SECONDS);
    }
}

Because tasks are stored in Redis data structures, loss is rare compared with the key‑expiration method.

Netty HashedWheelTimer

HashedWheelTimer

implements a time‑wheel with configurable tick duration and wheel size. Tasks are hashed into slots; a single thread advances the wheel and executes due tasks.

@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(timeout -> log.info("Execute delayed task"), 5, TimeUnit.SECONDS);
    }
}

Like JDK Timer, it suffers from single‑thread bottlenecks for long‑running tasks.

Hutool SystemTimer

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

Internally Hutool also uses a time‑wheel implementation.

Quartz

Quartz provides a full‑featured scheduler with Job, JobDetail, Trigger, and Scheduler components.

public class QuartzDemo {
    public static void main(String[] args) throws SchedulerException, InterruptedException {
        SchedulerFactory sf = new StdSchedulerFactory();
        Scheduler scheduler = sf.getScheduler();
        scheduler.start();
        JobDetail jb = JobBuilder.newJob(SanYouJob.class)
                .usingJobData("delayTask", "This is a delayed task")
                .build();
        Trigger t = TriggerBuilder.newTrigger()
                .startAt(DateUtil.offsetSecond(new Date(), 5))
                .build();
        log.info("Submit delayed task");
        scheduler.scheduleJob(jb, t);
    }
}

The scheduler runs a dedicated thread that moves tasks from the trigger to a thread pool when the fire time arrives.

Infinite Polling

@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("Process delayed task:{}", delayTask.taskContent);
                            DELAY_TASK_LIST.remove(delayTask);
                        }
                    }
                    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;
        }
    }
}

This approach is simple but inefficient because it scans the entire list on each tick.

Conclusion

All example code is available at the following repository:

https://github.com/sanyou3/delay-task-demo.git

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.

RedisRabbitMQRocketMQRedissonTimerQuartzScheduledThreadPoolExecutorDelayQueue
Shepherd Advanced Notes
Written by

Shepherd Advanced Notes

Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.

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.