Backend Development 26 min read

11 Ways to Implement Delayed Tasks in Java – From DelayQueue to Redis and MQ

This article surveys eleven practical techniques for implementing delayed tasks in Java, covering JDK utilities like DelayQueue, Timer, and ScheduledThreadPoolExecutor, as well as message‑queue solutions such as RocketMQ, RabbitMQ, Redis key‑expiration, Redisson, Netty, Hutool, Quartz, and simple polling, with code demos and implementation principles.

Sanyou's Java Diary
Sanyou's Java Diary
Sanyou's Java Diary
11 Ways to Implement Delayed Tasks in Java – From DelayQueue to Redis and MQ

Delay tasks are common in everyday systems, such as order‑cancellation after payment timeout or automatic receipt confirmation. This article reviews eleven implementation approaches, describing their principles and providing runnable demos.

DelayQueue

DelayQueue is a JDK API that implements 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 so the earliest expires first.

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>

Task parameters:

taskContent: the content of the delayed task

delayTime: delay in seconds

Test code creates a DelayQueue&lt;SanYouTask&gt; , starts a consumer thread that calls take() , and offers three tasks with delays of 5 s, 3 s and 8 s. The result shows tasks are processed in order of their delay.

Implementation principle

The offer method inserts a task into the queue and sorts it using compareTo . The take method retrieves the head element, checks getDelay , and blocks until the delay expires.

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();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                log.info("Execute delayed task");
            }
        }, 5000);
    }
}
</code>

The schedule method submits a task with a 5 s delay. Internally a TimerTask stores nextExecutionTime , and a TaskQueue orders tasks by this timestamp. A dedicated TimerThread executes tasks when their time arrives. Timer is simple but not recommended in Alibaba guidelines because it uses a single thread and lacks exception handling.

ScheduledThreadPoolExecutor

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

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>

The executor uses a DelayedWorkQueue internally. When a task is submitted it is wrapped in a ScheduledFutureTask that implements Delayed . The queue orders tasks by their delay, and worker threads fetch the earliest task for execution.

RocketMQ

RocketMQ provides delayed messages with 18 predefined delay levels. The producer sets the delay level on the message; the broker moves the message to an internal schedule topic until the delay expires, then forwards it to the original topic.

Demo

<code>&lt;dependency&gt;
    &lt;groupId&gt;org.apache.rocketmq&lt;/groupId&gt;
    &lt;artifactId&gt;rocketmq-spring-boot-starter&lt;/artifactId&gt;
    &lt;version&gt;2.2.1&lt;/version&gt;
&lt;/dependency&gt;
</code>

Configuration (name‑server, producer group) and a controller that sends a delayed message to sanyouDelayTaskTopic with level 2 (≈5 s). A consumer annotated with @RocketMQMessageListener receives the message after the delay.

RabbitMQ

RabbitMQ can implement delayed tasks via dead‑letter queues. A queue is declared 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 finally to a consumer queue.

Demo

<code>&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-amqp&lt;/artifactId&gt;
    &lt;version&gt;2.2.5.RELEASE&lt;/version&gt;
&lt;/dependency&gt;
</code>

Configuration creates a direct exchange, a TTL queue, and binds them. The controller publishes a message without an explicit delay; the queue’s TTL handles the 5 s delay.

Redis key‑expiration listener

Redis publishes an __keyevent@*__:expired event when a key is removed after expiration. 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

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

The listener receives the expired key, but this approach has drawbacks: expiration events are triggered only after the key is actually cleared (lazy or periodic cleanup), messages are not persisted, and the channel delivers events for all keys, requiring additional filtering.

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 equal to now + delay , a list SANYOU as the target queue, and a channel to trigger moving ready tasks.

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);
        startDelayQueueConsumer();
    }
    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);
    }
}
</code>

A REST controller calls offerTask to add a 5 s delayed task; the consumer thread receives it from the target list.

Netty HashedWheelTimer

HashedWheelTimer implements a timing wheel. The wheel is divided into slots; each slot represents a time slice (e.g., 100 ms). Tasks are hashed to a slot based on their expiration time and executed by a single worker thread.

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

Hutool SystemTimer

Hutool’s SystemTimer is a wrapper around a timing‑wheel implementation.

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>

Quartz

Quartz is a full‑featured job scheduler. A Job implements the business logic, a Trigger defines the fire time, and a Scheduler orchestrates execution.

Demo

<code>public class SanYouJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobDataMap map = context.getJobDetail().getJobDataMap();
        log.info("Received delayed task: {}", map.get("delayTask"));
    }
}
public class QuartzDemo {
    public static void main(String[] args) throws SchedulerException {
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        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>

Infinite polling

A simple approach creates a shared list of DelayTask objects and a background thread that scans the list every 100 ms, executing tasks whose triggerTime has passed.

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 task : DELAY_TASK_LIST) {
                        if (task.triggerTime <= System.currentTimeMillis()) {
                            log.info("Process delayed task: {}", task.taskContent);
                            DELAY_TASK_LIST.remove(task);
                        }
                    }
                    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>

Conclusion

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

JavaRedisSchedulingMessage 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.