11 Approaches to Implementing Delayed Tasks in Java

Delayed tasks are common in real-world applications, such as canceling orders after payment timeout or automatically confirming deliveries. This article analyzes 11 different ways to implement delayed tasks, from implementation to underlying principles. Each approach has its own strengths and suits different scenarios.


DelayQueue

DelayQueue is a blocking queue provided by JDK that supports delayed element retrieval. The generic type must implement the Delayed interface, which extends Comparable.

The getDelay method returns the remaining time before the task can be executed. When the返回值 is less than zero, the delayed task has reached its execution time.

The compareTo method sorts tasks, ensuring the task with the earliest execution time is at the head of the queue.

Example

@Getter
public class ScheduledItem implements Delayed {
    private final String taskData;
    private final long executionTime;

    public ScheduledItem(String taskData, long delaySeconds) {
        this.taskData = taskData;
        this.executionTime = System.currentTimeMillis() + delaySeconds * 1000;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(executionTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed other) {
        return Long.compare(this.executionTime, ((ScheduledItem) other).executionTime);
    }
}

Testing

@Slf4j
public class DelayQueueExample {
    public static void main(String[] args) {
        DelayQueue<ScheduledItem> taskQueue = new DelayQueue<>();

        new Thread(() -> {
            while (true) {
                try {
                    ScheduledItem item = taskQueue.take();
                    log.info("Processing delayed task: {}", item.getTaskData());
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }).start();

        log.info("Submitting delayed tasks");
        taskQueue.offer(new ScheduledItem("Task A - 5 seconds", 5L));
        taskQueue.offer(new ScheduledItem("Task B - 3 seconds", 3L));
        taskQueue.offer(new ScheduledItem("Task C - 8 seconds", 8L));
    }
}

Implementation Principle

When submitting tasks via the offer method, tasks are sorted based on the compareTo implementation, placing the earliest task at the queue head.

When the take method retrieves tasks, it gets the element at the queue head—the task with the earliest execution time. It checks if the task needs immediate execution via the getDelay return value. If not, it waits until the remaining delay time expires, then returns the task.


Timer

Timer is another JDK-provided utility for scheduling tasks.

Example

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

Implementation Principle

Submitted tasks are wrapped as TimerTask objects, which contain a nextExecutionTime property indicating when the task should run. Timer maintains a TaskQueue that stores tasks sorted by nextExecutionTime to quickly retrieve the earliest task.

Timer internally uses a TimerThread to execute tasks that have reached their delay time—similar to the thread started in the DelayQueue example.

However, Timer has several issues noted in Alibaba's coding standards:

  • Timer uses a single thread; long-running tasks can delay other tasks
  • Timer doesn't handle runtime exceptions; if one task throws an exception, the entire Timer fails

ScheduledThreadPoolExecutor

Due to Timer's limitations, ScheduledThreadPoolExecutor was introduced in JDK 1.5. It provides similar functionality to Timer but resolves the single-thread and exception-handling issues.

Example

@Slf4j
public class ScheduledExecutorExample {
    public static void main(String[] args) {
        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(
            2, 
            new ThreadPoolExecutor.CallerRunsPolicy()
        );

        log.info("Scheduling delayed task");
        executor.schedule(() -> log.info("Executing delayed task"), 5, TimeUnit.SECONDS);
    }
}

Implementation Principle

ScheduledThreadPoolExecutor extends ThreadPoolExecutor, leveraging multiple threads for task execution. It uses a DelayedWorkQueue as its internal blocking queue.

When submitting delayed tasks, they are wrapped as ScheduledFutureTask objects and placed into the DelayedWorkQueue. Since ScheduledFutureTask implements the Delayed interface, the queue sorts tasks by execution time, allowing threads to retrieve the earliest executable task.


RocketMQ

RocketMQ is an open-source message middleware that supports delayed messages. It offers 18 default delay levels.

Example

pom.xml dependencies:

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.2.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.2.5.RELEASE</version>
</dependency>

Configuration:

rocketmq:
  name-server: 192.168.200.144:9876
  producer:
    group: myProducer

Producer controller:

@RestController
@Slf4j
public class RocketMQDelayController {
    @Resource
    private DefaultMQProducer producer;

    @GetMapping("/rocketmq/add")
    public void addTask(@RequestParam("task") String task) throws Exception {
        Message msg = new Message("delayTaskTopic", "TagA", task.getBytes(RemotingHelper.DEFAULT_CHARSET));
        msg.setDelayTimeLevel(2); // Delay level 2 = 5 seconds
        log.info("Submitting delayed task");
        producer.send(msg);
    }
}

Consumer:

@Component
@RocketMQMessageListener(consumerGroup = "myConsumer", topic = "delayTaskTopic")
@Slf4j
public class DelayTaskListener implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        log.info("Received delayed task: {}", message);
    }
}

Implementation Principle

When the producer sends a delayed message, RocketMQ checks if the delay time level is greater than zero. If so, it transforms the message topic to SCHEDULE_TOPIC_XXXX for internal storage.

RocketMQ runs an internal scheduled task that polls messages from SCHEDULE_TOPIC_XXXX. When a message's delay time has passed, it moves the message to the original target topic where consumers can receive it.

This approach is more reliable than in-memory solutions because messages are persisted, surviving server restarts.


RabbitMQ

RabbitMQ can implement delayed tasks using dead letter queues.

Example

pom.xml dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
    <version>2.2.5.RELEASE</version>
</dependency>

Configuration:

spring:
  rabbitmq:
    host: 192.168.200.144
    port: 5672
    virtual-host: /

Queue configuration:

@Configuration
public class RabbitMQConfig {
    @Bean
    public DirectExchange mainExchange() {
        return new DirectExchange("mainExchange");
    }

    @Bean
    public Queue mainQueue() {
        return QueueBuilder
            .durable("mainQueue")
            .ttl(5000)
            .deadLetterExchange("delayExchange")
            .build();
    }

    @Bean
    public Binding mainBinding() {
        return BindingBuilder.bind(mainQueue()).to(mainExchange()).with("");
    }

    @Bean
    public DirectExchange delayExchange() {
        return new DirectExchange("delayExchange");
    }

    @Bean
    public Queue delayQueue() {
        return QueueBuilder.durable("delayQueue").build();
    }

    @Bean
    public Binding delayBinding() {
        return BindingBuilder.bind(delayQueue()).to(delayExchange()).with("");
    }
}

Producer:

@RestController
@Slf4j
public class RabbitMQDelayController {
    @Resource
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/rabbitmq/add")
    public void addTask(@RequestParam("task") String task) {
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        log.info("Submitting delayed task");
        rabbitTemplate.convertAndSend("mainExchange", "", task, correlationData);
    }
}

Implementation Principle

The workflow:

  1. Producer sends message to mainExchange
  2. Message routes to mainQueue (bound to mainExchange)
  3. Since mainQueue has no consumer and TTL is set to 5 seconds, the message becomes a dead letter after expiration
  4. Dead letter routes to delayExchange, then to delayQueue
  5. Consumer receives the message from delayQueue

RabbitMQ also provides a delay plugin that stores delayed messages in Mnesia, with a scheduled task checking and delivering messages when their delay expires.


Redis Key Expiration Listener

Redis supports pub/sub messaging. Redis publishes events to specific channels, including __keyevent@<db>__:expired when a key expires.

Example

pom.xml dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.2.5.RELEASE</version>
</dependency>

Redis configuration:

spring:
  redis:
    host: 192.168.200.144
    port: 6379

Configuration class:

@Configuration
public class RedisConfig {
    @Bean
    public RedisMessageListenerContainer listenerContainer(RedisConnectionFactory factory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(factory);
        return container;
    }

    @Bean
    public KeyExpirationEventMessageListener expirationListener(RedisMessageListenerContainer container) {
        return new KeyExpirationEventMessageListener(container);
    }
}

Event listener:

@Component
public class RedisKeyExpiredListener implements ApplicationListener<RedisKeyExpiredEvent> {
    @Override
    public void onApplicationEvent(RedisKeyExpiredEvent event) {
        byte[] source = event.getSource();
        System.out.println("Received delayed task: " + new String(source));
    }
}

Testing (via Redis CLI):

SET mytask taskdata
EXPIRE mytask 5

Known Issues

  1. Delayed Notification: Redis doesn't publish expiration events immediately when keys expire. It uses lazy expriation (key cleared on access) or periodic background cleanup. Tasks may not trigger at the exact delay time.

  2. Message Loss: Pub/sub has no persistence. If no client is subscribed when a message is published, it's lost.

  3. Broadcast Mode: All subscribers receive all messages. With multiple service instances, duplicate processing must be handled.

  4. All Keys Tracked: The listener receives all expired keys, not just specific ones. Filtering logic is needed.


Redisson RDelayedQueue

Redisson (Redis son) provides RDelayedQueue based on Redis data structures.

Example

pom.xml dependency:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.1</version>
</dependency>

Queue implementation:

@Component
@Slf4j
public class RedissonDelayQueue {
    private RedissonClient redissonClient;
    private RDelayedQueue<String> delayQueue;
    private RBlockingQueue<String> blockingQueue;

    @PostConstruct
    public void init() {
        Config config = new Config();
        SingleServerConfig serverConfig = config.useSingleServer();
        serverConfig.setAddress("redis://localhost:6379");
        redissonClient = Redisson.create(config);

        blockingQueue = redissonClient.getBlockingQueue("MY_QUEUE");
        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();
                }
            }
        }, "Consumer-Thread").start();
    }

    public void addTask(String task, long seconds) {
        log.info("Adding task: {}, delay: {}s", task, seconds);
        delayQueue.offer(task, seconds, TimeUnit.SECONDS);
    }
}

Controller:

@RestController
public class TaskController {
    @Resource
    private RedissonDelayQueue delayQueue;

    @GetMapping("/add")
    public void addTask(@RequestParam("task") String task) {
        delayQueue.addTask(task, 5);
    }
}

Implementation Principle

Redisson uses several Redis data structures:

  • redisson_delay_queue_timeout:MY_QUEUE - sorted set storing tasks sorted by execution timestamp
  • MY_QUEUE - list acting as the target queue for tasks whose delay has expired
  • redisson_delay_queue_channel:MY_QUEUE - channel for notifying clients

When submitting a task, Redisson adds it to the sorted set with a score of (current time + delay). An internal watcher monitors the channel and moves expired tasks to the target queue.

This is more reliable than the key expiration listener since tasks are persisted in Redis data structures.


Netty HashedWheelTimer

Netty provides HashedWheelTimer for efficient timer implementations using a time wheel algorithm.

Example

@Slf4j
public class HashedWheelTimerExample {
    public static void main(String[] args) {
        HashedWheelTimer timer = new HashedWheelTimer(100, TimeUnit.MILLISECONDS, 8);
        timer.start();

        log.info("Scheduling delayed task");
        timer.newTimeout(timeout -> log.info("Executing delayed task"), 5, TimeUnit.SECONDS);
    }
}

Implementation Principle

The time wheel is divided into multiple buckets (8 in the example). Each bucket represents a time slice (100ms in the example), so one full rotation takes 800ms.

When a task is submitted, its expiration time is hashed to determine the specific bucket. Tasks in each bucket are stored in a linked list.

A background thread continuously polls each bucket and executes tasks whose delay has expired.

Similar to Timer, HashedWheelTimer uses a single thread, so long-running tasks can delay others.


Hutool SystemTimer

Hutool prvoides SystemTimer, which also uses the time wheel algorithm internally.

Example

@Slf4j
public class SystemTimerExample {
    public static void main(String[] args) {
        SystemTimer systemTimer = new SystemTimer();
        systemTimer.start();

        log.info("Scheduling delayed task");
        systemTimer.addTask(new TimerTask(() -> log.info("Executing delayed task"), 5000));
    }
}

Quartz

Quartz is an open-source job scheduling framework that can implement delayed tasks.

Example

pom.xml dependency:

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.2</version>
</dependency>

Job implementation:

@Slf4j
public class CustomJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobDetail detail = context.getJobDetail();
        JobDataMap dataMap = detail.getJobDataMap();
        log.info("Received delayed task: {}", dataMap.get("taskData"));
    }
}

Scheduler setup:

public class QuartzExample {
    public static void main(String[] args) throws SchedulerException {
        SchedulerFactory factory = new StdSchedulerFactory();
        Scheduler scheduler = factory.getScheduler();
        scheduler.start();

        JobDetail job = JobBuilder.newJob(CustomJob.class)
            .usingJobData("taskData", "This is a delayed task")
            .build();

        Trigger trigger = TriggerBuilder.newTrigger()
            .startAt(DateUtil.offsetSecond(new Date(), 5))
            .build();

        log.info("Scheduling delayed task");
        scheduler.scheduleJob(job, trigger);
    }
}

Implementation Principle

Core components:

  • Job: Task definition with execute logic
  • JobDetail: Job metadata and parameters
  • Trigger: Defines when to trigger job execution
  • Scheduler: Registers and schedules jobs

The scheduler runs a QuartzSchedulerThread that checks if jobs have reached their execution time and submits them to the thread pool.


Polling Approach

The simplest approach: start a thread that continuously polls tasks and executes those whose delay has expired.

Example

@Slf4j
public class PollingExample {
    private static final List<PolledTask> TASK_LIST = new CopyOnWriteArrayList<>();

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                try {
                    for (PolledTask task : TASK_LIST) {
                        if (task.getTriggerTime() <= System.currentTimeMillis()) {
                            log.info("Processing task: {}", task.getContent());
                            TASK_LIST.remove(task);
                        }
                    }
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        }).start();

        log.info("Adding delayed task");
        TASK_LIST.add(new PolledTask("Sample task", 5L));
    }

    @Data
    @AllArgsConstructor
    public static class PolledTask {
        private String content;
        private long triggerTime;

        public PolledTask(String content, long delaySeconds) {
            this.content = content;
            this.triggerTime = System.currentTimeMillis() + delaySeconds * 1000;
        }
    }
}

This approach is simple but inefficient, especially with many tasks, as it requires iterating through all tasks on each poll.

Tags: java Delayed Tasks Timer Message Queue Redis

Posted on Thu, 21 May 2026 19:27:12 +0000 by Bhaal