This is part 10 of this series. If you missed any of the last nine, you can find them here:
- Part 1 – Overview
- Part 2 – Lifecycle of threads
- Part 3 – Daemon threads
- Part 4 – Interrupting threads
- Part 5 – Thread synchronization
- Part 6 – Runnable vs Callable
- Part 7 – Executors and Executor Services
- Part 8 – TimeUnits and Futures
- Part 9 – Shutting down executors and scheduling regular tasks
Introduction
In the last post we learned how to gracefully shut down Executor and ExecutorService objects. We also looked at scheduling tasks to run regularly with the Timer and TimerTask classes.
In this post we’re also going to look at scheduling tasks to run regularly. However, we’ll be using the ScheduledThreadPoolExecutor to do this.
Problems with Timers and TimerTasks
We saw that using the Timer and TimerTask classes was easy enough, but there were some problems.
The biggest issue was that we have to extend the TimerTask class. TimerTask is an abstract class, and not a functional interface. This means that we cannot use lambda expressions to create TimerTask tasks. We must create tasks as top-level, named inner or anonymous inner classes.
Another (possibly less) concerning problem is that each Timer object has only a single background thread that is used to sequentially execute all of the scheduled TimerTasks. If a task takes too long to execute, it can “hog” the single timer thread.
ScheduledThreadPoolExecutor
Java 5 introduced the ScheduledThreadPoolExecutor class as a versatile replacement for the Timer and TimerTask combination. This is a thread pool that can execute tasks that run periodically and/or after a given delay. It accepts delays and repeat periods as TimeUnit values. This class is in the java.util.concurrent package.
What’s really nice about using the ScheduledThreadPoolExecutor is that we can submit Runnable and Callable tasks to it. We don’t need to create tasks that extend TimerTask; tasks only need to implement either the Runnable or Callable interface.
When creating a ScheduledThreadPoolExecutor, we can specify the number of internal service threads in the core thread pool. A ScheduledThreadPoolExecutor configured with a single thread is the same as using a Timer object.
We’ve seen how to submit a task to an ExecutorService implementation. These tasks run to completion and then terminate. If we want to schedule a task that has to run periodically, we can use an implementation of the ScheduledExecutorService interface. Currently ScheduledThreadPoolExecutor is the only concrete implementation provided in the JSE API.
Scheduling Tasks
The ScheduledThreadPoolExecutor is just another implementation of an ExecutorService with the added functionality of being able to schedule tasks to run regularly and/or after a given delay. It extends both the Executor and ExecutorService interfaces.
We have a number of ways to schedule/submit tasks to the ScheduledThreadPoolExecutor to run. At first it’s a bit confusing when we first read the API documentation. There are five methods to be aware of:
- The
execute()method. - The overloaded
submit()methods. - The overloaded
schedulemethods. - The
scheduleAtFixedRate()andscheduleWithFixedDelay()methods.
The execute() method
We use the execute() method to run a Runnable task. This method is specified in the Executor interface. The execute() method returns void because it executes a Runnable task. We’ve seen and used this method before in an earlier post.
The submit() methods.
We can submit Runnable and Callable tasks using the standard submit() methods defined in the ExecutorService interface. These method return Future values. We’ve seen and used these methods before in an earlier post.
The schedule() methods.
The schedule methods are defined in the ScheduledExecutorService interface. The schedule() methods return a ScheduledFuture value.
Runnable and Callable tasks can be scheduled for one-time execution with or without an initial delay by using the overloaded schedule() methods. These tasks are often referred to as one-shot actions.
The delay and repeat period parameters passed to the schedule() methods are calculated relative to when the task is scheduled. These are not absolute dates/times. It is a simple matter to transform an absolute time represented as a Date to the required form. For example, to schedule at a certain future date, we can use:
Calendar calendar = new GregorianCalendar(year, month, day, hour, minute, second);
Date date = calendar.getTime();
schedule(task, date.getTime()-System.currentTimeMillis(), TimeUnit.MILLISECONDS);
Using the execute() method is the same as calling schedule() with a zero delay.
The scheduleAtFixedRate() and scheduleWithFixedDelay() methods
The scheduleAtFixedRate() and scheduleWithFixedDelay() methods are defined in the ScheduledExecutorService interface. The methods do exactly what their names imply, and return a ScheduledFuture value.
We can only schedule Runnable tasks using these methods. The tasks can be scheduled for repeated execution at regular intervals with or without an initial delay.
The scheduleAtFixedRate() method executes tasks with a fixed time rate with or without a delay. The following code schedules a task every two seconds with an initial delay of one second:
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
Runnable task = () -> System.out.println("Task running at "
+ System.currentTimeMillis());
int delay = 1;
int rate = 2;
executor.scheduleAtFixedRate(task, delay, rate, TimeUnit.SECONDS);
The scheduleAtFixedRate() method doesn’t take into account the duration of the task. If the execution of the task takes longer than its repeat interval, then the following executions may start late, but will not execute concurrently.
The scheduleWithFixedDelay() method works very much like scheduleAtFixedRate(), but the wait time period applies between the end of a task and the start of the next task.
The following code schedules a task with a duration of two seconds. There is no initial delay, and a fixed delay of one second between the end of an execution and the start of the next execution. This results in the task being executed every three seconds (the delay plus the task execution time). In other words, it will run at 0 seconds, 3 seconds, 6 seconds and so on. The scheduleWithFixedDelay() method is useful if the duration of the scheduled tasks is unpredictable.
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(2);
long time = System.currentTimeMillis();
System.out.println("Task running at " + time);
}
catch (InterruptedException e) {
System.err.println("Task interrupted!");
}
};
int delay = 0;
int rate = 1;
executor.scheduleWithFixedDelay(task, delay, rate, TimeUnit.SECONDS);
If we change the last line to:
executor.scheduleAtFixedRate(task, delay, rate, TimeUnit.SECONDS);
then the task will run every two seconds, which is simply the length of the time of the task execution. A new task will not be created to overlap the running of the original task.
Example
Here’s a Java program very similar to the program in the last post, but using a ScheduledExecutorService rather than a Timer. Refer to the JE API documentation for more details.
import java.util.List;
import java.util.Date;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ScheduledExecutorServiceTest {
public static void main(String args[]) throws InterruptedException {
// Creating a ScheduledExecutorService
ScheduledExecutorService executor;
executor = Executors.newScheduledThreadPool(5); // pooled
// OR using only a single thread
// executor = Executors.newSingleThreadScheduledExecutor();
// Creating tasks using lambda expressions.
Runnable task1 = () -> System.out.println("Regular task ran at " +
System.currentTimeMillis());
Runnable task2 = () -> System.out.println("Delayed task ran at " +
new Date().toString());
Runnable task3 = () -> System.out.println("Task 3 running at " +
System.currentTimeMillis());
Runnable task4 = () -> System.out.println("Task 4 running at " +
System.currentTimeMillis());
// Scheduling the tasks
// first parameter is obviously the task
// second parameter is the delay in a TimeUnit
// third parameter is the interval in a TimeUnit
int rate = 2;
executor.scheduleAtFixedRate(task1, 1, rate, TimeUnit.SECONDS);
executor.scheduleAtFixedRate(task3, 0, rate*2, TimeUnit.SECONDS);
executor.scheduleAtFixedRate(task4, 0, rate*3, TimeUnit.SECONDS);
// Assign appropriate values to these variables to set a time
// a few seconds in the future for testing fixed time start.
int year, month, day, hour, minute, second;
Calendar calendar = new GregorianCalendar(year, month, day,
hour, minute, second);
Date date = calendar.getTime();
long currentTime = System.currentTimeMillis();
executor.schedule(task2, date.getTime() - currentTime,
TimeUnit.MILLISECONDS);
// Unrelated Thread stuff - for interest
System.out.printf("%n");
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
System.out.printf("Thread group: %s%n", threadGroup);
int activeThreads = threadGroup.activeCount();
System.out.printf("Active thread count: %d%n", activeThreads);
Thread threads[] = new Thread[activeThreads];
threadGroup.enumerate(threads);
for (Thread t : threads) {
System.out.printf("Thread name: %s%n", t.getName());
}
System.out.printf("%n");
// Sleeping the main thread to simulate a running application
TimeUnit.SECONDS.sleep(25);
// Cancel all the tasks
try {
System.out.println("Shutting down the executor service...");
// No new tasks can be submitted,
// but previously submitted tasks will be run.
executor.shutdown();
// Wait for 10 seconds for existing tasks to finish
executor.awaitTermination(10, TimeUnit.SECONDS);
}
catch (InterruptedException e) {
System.err.println("Tasks were interrupted!");
}
finally {
// If all tasks completed following shutdown(), returns true
if (!executor.isTerminated()) {
System.err.println("Cancelling any incomplete tasks...");
}
// Tries to stop all executing tasks,
// and stops processing of waiting tasks
executor.shutdownNow();
System.out.println("Shutdown done!");
}
}
} // end of class
The somewhat unrelated Thread code in the program is the same as the last post. The interesting part is that as we increase the size of the thread pool e.g., Executors.newScheduledThreadPool(5), we will obviously see more threads in the output:
Thread group: java.lang.ThreadGroup[name=main,maxpri=10]
Active thread count: 6
Thread name: main
Thread name: pool-1-thread-1
Thread name: pool-1-thread-2
Thread name: pool-1-thread-3
Thread name: pool-1-thread-4
Thread name: pool-1-thread-5
We can also use Executors.newSingleThreadScheduledExecutor() to create a ScheduledExecutorService that uses a single thread to process the tasks.
Ending Off
By this stage, you’re probably all tired of threads, so from the next post, we’ll look at something else interesting Java related. There are always lots and lots of interesting Java topics…!
Please share your comments, tell me about your experiences, and ask your questions!