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.
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
HashedWheelTimerimplements 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
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Shepherd Advanced Notes
Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
