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:
- Producer sends message to mainExchange
- Message routes to mainQueue (bound to mainExchange)
- Since mainQueue has no consumer and TTL is set to 5 seconds, the message becomes a dead letter after expiration
- Dead letter routes to delayExchange, then to delayQueue
- 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
-
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.
-
Message Loss: Pub/sub has no persistence. If no client is subscribed when a message is published, it's lost.
-
Broadcast Mode: All subscribers receive all messages. With multiple service instances, duplicate processing must be handled.
-
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 timestampMY_QUEUE- list acting as the target queue for tasks whose delay has expiredredisson_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.