11 Ways to Implement Delayed Tasks in Java: From DelayQueue to Quartz
This article surveys eleven Java techniques for delayed task execution—including DelayQueue, Timer, ScheduledThreadPoolExecutor, RocketMQ, RabbitMQ, Redis key‑expiration, Redisson, Netty, Hutool, Quartz and simple polling—explaining their core principles, code examples, and when each approach is appropriate.
Delay tasks are common in everyday development, such as order timeout cancellation or automatic receipt confirmation. This article reviews eleven implementation methods, discussing their principles and suitable scenarios.
DelayQueue
DelayQueue is a JDK API that provides a delayed queue. The generic type must implement the Delayed interface, which extends Comparable. The getDelay method returns the remaining time before execution; a negative value means the task is ready. The compareTo method orders tasks so the earliest one is at the head of the queue.
Demo
@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);
}
}SanYouTask implements Delayed. Its constructor parameters are:
taskContent: the specific content of the delayed task
delayTime: delay duration in seconds
Test
@Slf4j
public class DelayQueueDemo {
public static void main(String[] args) {
DelayQueue<SanYouTask> queue = new DelayQueue<>();
new Thread(() -> {
while (true) {
try {
SanYouTask task = queue.take();
log.info("Got delayed task:{}", task.getTaskContent());
} catch (Exception e) {}
}
}).start();
log.info("Submitting delayed tasks");
queue.offer(new SanYouTask("Task 5s", 5L));
queue.offer(new SanYouTask("Task 3s", 3L));
queue.offer(new SanYouTask("Task 8s", 8L));
}
}The thread takes tasks from the queue; the three tasks are executed after 3, 5, and 8 seconds respectively.
Timer
Timer is another JDK API for delayed execution.
Demo
@Slf4j
public class TimerDemo {
public static void main(String[] args) {
Timer timer = new Timer();
log.info("Submitting delayed task");
timer.schedule(new TimerTask() {
@Override
public void run() {
log.info("Executing delayed task");
}
}, 5000);
}
}Timer uses a single thread and does not handle runtime exceptions, which can cause the whole timer to crash. Therefore it is not recommended in Alibaba’s coding guidelines.
ScheduledThreadPoolExecutor
Since JDK 1.5, ScheduledThreadPoolExecutor provides a thread‑pool‑based replacement for Timer, solving the single‑thread and exception‑crash problems.
Demo
@Slf4j
public class ScheduledThreadPoolExecutorDemo {
public static void main(String[] args) {
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2, new ThreadPoolExecutor.CallerRunsPolicy());
log.info("Submitting delayed task");
executor.schedule(() -> log.info("Executing delayed task"), 5, TimeUnit.SECONDS);
}
}The executor uses a DelayedWorkQueue internally; tasks are wrapped as ScheduledFutureTask objects and ordered by their delay.
RocketMQ
RocketMQ, an Alibaba‑open‑source message middleware, supports delayed messages with 18 predefined delay levels. The producer sets the delay level when sending a message.
Demo
@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 seconds
log.info("Submitting delayed task");
producer.send(msg);
}
}A consumer listening to sanyouDelayTaskTopic receives the delayed message after the configured delay.
RabbitMQ
RabbitMQ can implement delayed tasks using dead‑letter queues. The queue is configured with a TTL (time‑to‑live) and a dead‑letter exchange.
Demo
@RestController
@Slf4j
public class RabbitMQDelayTaskController {
@Resource
private RabbitTemplate rabbitTemplate;
@GetMapping("/rabbitmq/add")
public void addTask(@RequestParam("task") String task) throws Exception {
CorrelationData cd = new CorrelationData(UUID.randomUUID().toString());
log.info("Submitting delayed task");
rabbitTemplate.convertAndSend("sanyouDirectExchangee", "", task, cd);
}
}The message first lands in a TTL‑configured queue; after expiration it is routed to the dead‑letter exchange and finally to the consumer queue.
Listening to Redis Expired Keys
Redis publishes an event on the channel __keyevent@<db>__:expired when a key expires. By storing delayed tasks as keys with the desired TTL and listening to this channel, a consumer can react to the expiration.
Demo
@Component
public class MyRedisKeyExpiredEventListener implements ApplicationListener<RedisKeyExpiredEvent> {
@Override
public void onApplicationEvent(RedisKeyExpiredEvent event) {
byte[] body = event.getSource();
System.out.println("Got delayed message: " + new String(body));
}
}Note that Redis may use lazy or periodic expiration, so a key might not fire the event exactly at the intended time, and the pub/sub model does not persist messages.
Redisson RDelayedQueue
Redisson provides a distributed delayed queue built on Redis data structures.
Demo
@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);
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("Adding delayed task:{} with {}s", task, seconds);
delayQueue.offer(task, seconds, TimeUnit.SECONDS);
}
}The implementation uses a sorted set redisson_delay_queue_timeout:SANYOU to store tasks with their execution timestamps, a list SANYOU as the final queue, and a channel to trigger moving ready tasks.
Netty HashedWheelTimer
Netty’s HashedWheelTimer implements a timing wheel. The wheel is divided into slots; each task is hashed to a slot based on its delay. A background thread scans slots and executes due tasks.
Demo
@Slf4j
public class NettyHashedWheelTimerDemo {
public static void main(String[] args) {
HashedWheelTimer timer = new HashedWheelTimer(100, TimeUnit.MILLISECONDS, 8);
timer.start();
log.info("Submitting delayed task");
timer.newTimeout(timeout -> log.info("Executing delayed task"), 5, TimeUnit.SECONDS);
}
}Hutool SystemTimer
Hutool’s SystemTimer is a wrapper around a timing‑wheel implementation.
Demo
@Slf4j
public class SystemTimerDemo {
public static void main(String[] args) {
SystemTimer systemTimer = new SystemTimer();
systemTimer.start();
log.info("Submitting delayed task");
systemTimer.addTask(new TimerTask(() -> log.info("Executing delayed task"), 5000));
}
}Quartz
Quartz is a full‑featured job‑scheduling framework. A Job implements the execution logic, a Trigger defines the fire time, and a Scheduler orchestrates them.
Demo
public class SanYouJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap map = context.getJobDetail().getJobDataMap();
log.info("Got delayed task:{}", map.get("delayTask"));
}
}
public class QuartzDemo {
public static void main(String[] args) throws SchedulerException {
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler scheduler = sf.getScheduler();
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("Submitting delayed task");
scheduler.scheduleJob(job, trigger);
}
}Infinite Polling
A simple approach is to keep a thread that continuously scans a list of tasks and executes those whose trigger time has passed.
Demo
@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.getTriggerTime() <= System.currentTimeMillis()) {
log.info("Processing delayed task:{}", task.getTaskContent());
DELAY_TASK_LIST.remove(task);
}
}
TimeUnit.MILLISECONDS.sleep(100);
} catch (Exception e) {}
}
}).start();
log.info("Submitting 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 method is easy but inefficient because it scans all tasks each cycle.
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.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.
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.
