In Java, the scheduler is used to schedule a thread or task that executes at a certain period of time or periodically at a fixed interval.
What Is the Java Scheduler?
The scheduler is used in Java to schedule a thread or task that can execute at a specified time or at a fixed interval. There are four common ways to schedule a task in Java, including:
java.util.TimerTask
,java.util.concurrent.ScheduledExecutorService
,- the Quartz scheduler and
org.springframework.scheduling.TaskScheduler
.
In this article, we are going to cover the following topics pertaining to the Java scheduler:
- How to schedule a task in Java.
SchedularConfigurer
vs.@Scheduled
.- Changing the
cron
expression dynamically. - Dependency execution between two tasks.
How to Scheduler a Task in Java
There are multiple ways to schedule a task in Java, including:
-
java.util.TimerTask
java.util.concurrent.ScheduledExecutorService
- Quartz scheduler
-
org.springframework.scheduling.TaskScheduler
TimerTask
is executed by a demon thread. Any delay in a task can delay the other task in a schedule. Hence, it is not a viable option when multiple tasks need to be executed asynchronously at a certain time.
Let’s look at an example:
package com.example.timerExamples;
import java.util.Timer;
public class ExecuteTimer {
public static void main(String[] args){
TimerExample te1=new TimerExample("Task1");
TimerExample te2=new TimerExample("Task2");
Timer t=new Timer();
t.scheduleAtFixedRate(te1, 0,5*1000);
t.scheduleAtFixedRate(te2, 0,1000);
}
}
public class TimerExample extends TimerTask{
private String name ;
public TimerExample(String n){
this.name=n;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "+name+" the task has executed successfully "+ new Date());
if("Task1".equalsIgnoreCase(name)){
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
Output:
Timer-0 Task1 the task has executed successfully Wed Nov 14 14:32:49 GMT 2018
Timer-0 Task2 the task has executed successfully Wed Nov 14 14:32:59 GMT 2018
Timer-0 Task2 the task has executed successfully Wed Nov 14 14:32:59 GMT 2018
Timer-0 Task2 the task has executed successfully Wed Nov 14 14:32:59 GMT 2018
Timer-0 Task2 the task has executed successfully Wed Nov 14 14:32:59 GMT 2018
Timer-0 Task2 the task has executed successfully Wed Nov 14 14:32:59 GMT 2018
Timer-0 Task2 the task has executed successfully Wed Nov 14 14:32:59 GMT 2018
Timer-0 Task1 the task has executed successfully Wed Nov 14 14:32:59 GMT 2018
Timer-0 Task2 the task has executed successfully Wed Nov 14 14:33:09 GMT 2018
Timer-0 Task2 the task has executed successfully Wed Nov 14 14:33:09 GMT 2018
Timer-0 Task2 the task has executed successfully Wed Nov 14 14:33:09 GMT 2018
Timer-0 Task2 the task has executed successfully Wed Nov 14 14:33:09 GMT 2018
Timer-0 Task2 the task has executed successfully Wed Nov 14 14:33:09 GMT 2018
Timer-0 Task1 the task has executed successfully Wed Nov 14 14:33:09 GMT 2018
In the above execution, it is clear that task 2 gets stuck because the thread that is handling task1 is going to sleep for 10 seconds. Hence, there is only one demon thread that is working on both task 1 and task 2, and if one gets hit, all the tasks will be pushed back.
ScheduledExecutorService
and TaskScheduler
works in the same manner. The only difference from the former is the Java library and the latter is the Spring framework. So if the application is in Spring, the TaskScheduler
can be a better option to schedule jobs.
Now, let’s see the usage of the TaskScheduler
interface and we can use it in the Spring framework.
SchedulingConfigurer Vs. @Scheduled
Spring provides an annotation-based scheduling operation with the help of @Scheduled
.
The threads are handled by the Spring framework, which means we won’t have any control over the threads that work on the tasks. Let’s take a look at the example below:
@Configuration
@EnableScheduling
public class ScheduledConfiguration {
@Scheduled(fixedRate = 5000)
public void executeTask1() {
System.out.println(Thread.currentThread().getName()+" The Task1 executed at "+ new Date());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Scheduled(fixedRate = 1000)
public void executeTask2() {
System.out.println(Thread.currentThread().getName()+" The Task2 executed at "+ new Date());
}
}
Output:
scheduling-1 The Task2 executed at Wed Nov 14 14:22:59 GMT 2018
scheduling-1 The Task2 executed at Wed Nov 14 14:22:59 GMT 2018
scheduling-1 The Task2 executed at Wed Nov 14 14:22:59 GMT 2018
scheduling-1 The Task2 executed at Wed Nov 14 14:22:59 GMT 2018
scheduling-1 The Task2 executed at Wed Nov 14 14:22:59 GMT 2018
scheduling-1 The Task1 executed at Wed Nov 14 14:22:59 GMT 2018
scheduling-1 The Task2 executed at Wed Nov 14 14:23:09 GMT 2018
scheduling-1 The Task2 executed at Wed Nov 14 14:23:09 GMT 2018
scheduling-1 The Task2 executed at Wed Nov 14 14:23:09 GMT 2018
scheduling-1 The Task2 executed at Wed Nov 14 14:23:09 GMT 2018
scheduling-1 The Task2 executed at Wed Nov 14 14:23:09 GMT 2018
scheduling-1 The Task1 executed at Wed Nov 14 14:23:09 GMT 2018
There is one thread, scheduling-1, that handles both task1 and task2. The moment task1 goes to sleep for 10 seconds, task 2 waits for it. In other words, if there are two jobs running at the same time, one will wait for the other to complete.
Now, we will try writing a scheduler task in which we want to execute task1 and task2 asynchronously. There will be a pool of threads, and we will schedule each task in the ThreadPoolTaskScheduler
. This class needs to implement the SchedulingConfigurer
interface. It gives more control to the scheduler threads compared to @Scheduled
.
This time, I’ll be creating two jobs: job1 and job2. Then, I will be scheduling it using TaskScheduler
. I’ll be using a Cron expression to schedule job1 at five-second intervals and job2 every second. In this scenario, if job1 gets stuck for 10 seconds, job2 will continue running without interruption. We see that both task1 and task 2 are being handled by a pool of threads, which is created using ThreadPoolTaskScheduler
.
@Configuration
@EnableScheduling
public class ScheduledConfiguration implements SchedulingConfigurer {
TaskScheduler taskScheduler;
private ScheduledFuture<?> job1;
private ScheduledFuture<?> job2;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler threadPoolTaskScheduler =new ThreadPoolTaskScheduler();
threadPoolTaskScheduler.setPoolSize(10);// Set the pool of threads
threadPoolTaskScheduler.setThreadNamePrefix("scheduler-thread");
threadPoolTaskScheduler.initialize();
job1(threadPoolTaskScheduler);// Assign the job1 to the scheduler
job2(threadPoolTaskScheduler);// Assign the job1 to the scheduler
this.taskScheduler=threadPoolTaskScheduler;// this will be used in later part of the article during refreshing the cron expression dynamically
taskRegistrar.setTaskScheduler(threadPoolTaskScheduler);
}
private void job1(TaskScheduler scheduler) {
job1 = scheduler.schedule(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " The Task1 executed at " + new Date());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}, new Trigger() {
@Override
public Date nextExecutionTime(TriggerContext triggerContext) {
String cronExp = "0/5 * * * * ?";// Can be pulled from a db .
return new CronTrigger(cronExp).nextExecutionTime(triggerContext);
}
});
}
private void job2(TaskScheduler scheduler){
job2=scheduler.schedule(new Runnable(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" The Task2 executed at "+ new Date());
}
}, new Trigger(){
@Override
public Date nextExecutionTime(TriggerContext triggerContext) {
String cronExp="0/1 * * * * ?";//Can be pulled from a db . This will run every minute
return new CronTrigger(cronExp).nextExecutionTime(triggerContext);
}
});
}
}
Output:
scheduler-thread1 The Task2 executed at Wed Nov 14 15:02:46 GMT 2018
scheduler-thread2 The Task2 executed at Wed Nov 14 15:02:47 GMT 2018
scheduler-thread3 The Task2 executed at Wed Nov 14 15:02:48 GMT 2018
scheduler-thread2 The Task2 executed at Wed Nov 14 15:02:49 GMT 2018
scheduler-thread1 The Task2 executed at Wed Nov 14 15:02:50 GMT 2018
scheduler-thread7 The Task1 executed at Wed Nov 14 15:02:50 GMT 2018
scheduler-thread3 The Task2 executed at Wed Nov 14 15:02:51 GMT 2018
scheduler-thread5 The Task2 executed at Wed Nov 14 15:02:52 GMT 2018
scheduler-thread2 The Task2 executed at Wed Nov 14 15:02:53 GMT 2018
scheduler-thread6 The Task2 executed at Wed Nov 14 15:02:54 GMT 2018
scheduler-thread6 The Task2 executed at Wed Nov 14 15:02:55 GMT 2018
scheduler-thread6 The Task2 executed at Wed Nov 14 15:02:56 GMT 2018
scheduler-thread6 The Task2 executed at Wed Nov 14 15:02:57 GMT 2018
scheduler-thread6 The Task2 executed at Wed Nov 14 15:02:58 GMT 2018
scheduler-thread6 The Task2 executed at Wed Nov 14 15:02:59 GMT 2018
scheduler-thread6 The Task2 executed at Wed Nov 14 15:03:00 GMT 2018
scheduler-thread2 The Task2 executed at Wed Nov 14 15:03:01 GMT 2018
scheduler-thread2 The Task2 executed at Wed Nov 14 15:03:02 GMT 2018
scheduler-thread2 The Task2 executed at Wed Nov 14 15:03:03 GMT 2018
scheduler-thread2 The Task2 executed at Wed Nov 14 15:03:04 GMT 2018
scheduler-thread10 The Task2 executed at Wed Nov 14 15:03:05 GMT 2018
scheduler-thread8 The Task1 executed at Wed Nov 14 15:03:05 GMT 2018-
Changing a Cron Expression Dynamically in Java Scheduler
We can always keep the cron
expression in a property file using Spring Config. If the Spring Config server is not available, we can also fetch it from the database. Any update of the cron
expression will update the scheduler. But in order to cancel the current schedule and execute the new schedule, we can expose an API to refresh the cron
job:
public void refreshCronSchedule(){
if(job1!=null){
job1.cancel(true);
scheduleJob1(taskScheduler);
}
if(job2!=null){
job2.cancel(true);
scheduleJob2(taskScheduler);
}
}
Additionally, you can invoke the method from any controller to refresh the cron
schedule.
Dependency Execution Between Two Tasks in Java Scheduler
So far, we know that we can execute the jobs asynchronously using the TaskScheduler
and Schedulingconfigurer
interface. Now, let’s say we have job1 that runs for an hour at 1 a.m., and job2 that runs at 2 a.m. However, we don’t want job2 to start unless job1 is complete. We also have another list of jobs that can run between 1 and 2 a.m. that are independent of other jobs.
Let’s see how we can create a dependency between job1 and job2, yet run all jobs asynchronously at the scheduled time.
First, let’s declare a volatile variable:
private volatile boolean job1Flag=false;
private void scheduleJob1(TaskScheduler scheduler) {
job1 = scheduler.schedule(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " The Task1 executed at " + new Date());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
job1Flag=true;// setting the flag true to mark it complete
}
}, new Trigger() {
@Override
public Date nextExecutionTime(TriggerContext triggerContext) {
String cronExp = "0/5 * * * * ?";// Can be pulled from a db
return new CronTrigger(cronExp).nextExecutionTime(triggerContext);
}
});
}
private void scheduleJob2(TaskScheduler scheduler) {
job2=scheduler.schedule(new Runnable(){
@Override
public void run() {
synchronized(this){
while(!job1Flag){
System.out.println(Thread.currentThread().getName()+" waiting for job1 to complete to execute "+ new Date());
try {
wait(1000);// add any number of seconds to wait
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(Thread.currentThread().getName()+" The Task2 executed at "+ new Date());
job1Flag=false;
}
}, new Trigger(){
@Override
public Date nextExecutionTime(TriggerContext triggerContext) {
String cronExp="0/5 * * * * ?";//Can be pulled from a db . This will run every minute
return new CronTrigger(cronExp).nextExecutionTime(triggerContext);
}
});
}
scheduler-thread2 The Task1 executed at Wed Nov 14 16:30:50 GMT 2018
scheduler-thread1 waiting for job1 to complete to execute Wed Nov 14 16:30:51 GMT 2018
scheduler-thread1 waiting for job1 to complete to execute Wed Nov 14 16:30:52 GMT 2018
scheduler-thread1 waiting for job1 to complete to execute Wed Nov 14 16:30:53 GMT 2018
scheduler-thread1 waiting for job1 to complete to execute Wed Nov 14 16:30:54 GMT 2018
scheduler-thread1 waiting for job1 to complete to execute Wed Nov 14 16:30:55 GMT 2018
scheduler-thread1 waiting for job1 to complete to execute Wed Nov 14 16:30:56 GMT 2018
scheduler-thread1 waiting for job1 to complete to execute Wed Nov 14 16:30:57 GMT 2018
scheduler-thread1 waiting for job1 to complete to execute Wed Nov 14 16:30:58 GMT 2018
scheduler-thread1 waiting for job1 to complete to execute Wed Nov 14 16:30:59 GMT 2018
scheduler-thread1 The Task2 executed at Wed Nov 14 16:31:00 GMT 2018
scheduler-thread2 The Task1 executed at Wed Nov 14 16:31:05 GMT 2018
scheduler-thread3 waiting for job1 to complete to execute Wed Nov 14 16:31:05 GMT 2018
scheduler-thread3 waiting for job1 to complete to execute Wed Nov 14 16:31:06 GMT 2018
scheduler-thread3 waiting for job1 to complete to execute Wed Nov 14 16:31:07 GMT 2018
scheduler-thread3 waiting for job1 to complete to execute Wed Nov 14 16:31:08 GMT 2018
scheduler-thread3 waiting for job1 to complete to execute Wed Nov 14 16:31:09 GMT 2018
scheduler-thread3 waiting for job1 to complete to execute Wed Nov 14 16:31:10 GMT 2018
scheduler-thread3 waiting for job1 to complete to execute Wed Nov 14 16:31:11 GMT 2018
scheduler-thread3 waiting for job1 to complete to execute Wed Nov 14 16:31:12 GMT 2018
scheduler-thread3 waiting for job1 to complete to execute Wed Nov 14 16:31:13 GMT 2018
scheduler-thread3 waiting for job1 to complete to execute Wed Nov 14 16:31:14 GMT 2018
scheduler-thread3 The Task2 executed at Wed Nov 14 16:31:15 GMT 2018
scheduler-thread1 The Task1 executed at Wed Nov 14 16:31:20 GMT 2018
We chose to use a volatile boolean flag so that it’s not cached in the thread-local but is saved in the main memory and can be used by all threads in the pool. Based on the flag, job2 waits indefinitely until job1 is complete. If job1 stalls, there is a chance that job2 will wait indefinitely.
There are various ways to schedule in Java, and now, we know how to use the scheduler API and the Spring scheduler API to control threads in the pool.