A Comprehensive Guide to Java Scheduler

The Java Scheduler is used to schedule a thread or task that executes at a certain period of time or periodically at a fixed interval.

Written by Joydip Kumar
Published on Feb. 07, 2023
Image: Shutterstock / Built In
Image: Shutterstock / Built In
Brand Studio Logo

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.

More on Software Engineering: How to make a JavaScript API Call

 

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.

Scheduling a task in Java using Quartz. | Video: Play Java

More on Software Engineering: Python Lists and List Manipulation Explained

 

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.                   

Explore Job Matches.