This is part 8 of this series. If you missed any of the last seven, 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
Introduction
In the last post we created Executor
and ExecutorService
objects, and submitted Callable
and Runnable
objects to them. We also had a brief look at accessing Callable
return values using the Future
interface.
In this post, we’ll introduce the TimeUnit
enum, and look at some of the other methods of the Future
interface.
The TimeUnit Enum
We’ve already seen that threads can be put to sleep for a certain length of time by calling the Thread.sleep()
method and passing in the required number of milliseconds for the thread to sleep. We could use this to simulate long running tasks, or just to wait until a computation is complete. For example, Thread.sleep(1000)
will cause the current thread to sleep for a second (1000 milliseconds).
A small problem with Thread.sleep()
is that we sometimes have to do some simple multiplication when setting the sleep time. If we wanted a thread to sleep for an hour, we’d have to multiply 1000 by 60 and by 60 gain to get the number of milliseconds in ah hour. We’d then call Thread.sleep(1000*60*60)
.
The Concurrency API introduced an enum
called TimeUnit
so that we can more easily work with units of time. The enum constants are NANOSECONDS
, MICROSECONDS
, MILLISECONDS
, SECONDS
, MINUTES
, HOURS
and DAYS
.
Each TimeUnit
constant has a number of methods including sleep()
, timedJoin()
, timedWait()
, and conversion methods to convert between the different units of time.
Here’s an example which shows how we can use the sleep()
method to give us a two second delay:
Runnable task = () -> {
try {
String name = Thread.currentThread().getName();
System.out.println("Before sleeping: " + name);
TimeUnit.SECONDS.sleep(2);
System.out.println("After sleeping: " + name);
}
catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(task);
thread.start();
Probably the three most useful methods of the TimeUnit
enum are convenience methods that wrap around other classes’ methods and convert the timeout argument into the required number of milliseconds and nanoseconds:
-
public void sleep(long timeout)
— wraps around theThread.sleep()
method. -
public void timedJoin(Thread thread, long timeout)
— wraps around theThread.join()
method. -
public void timedWait(Object object, long timeout)
— wraps around theObject.wait()
method.
These methods make our life a little easier. Instead of using Thread.sleep(1000*60*60)
, we can simply call TimeUnit.HOURS.sleep(1)
, and it’s also more obvious to the maintainers of our code.
The Future Interface
In last week’s post, we saw that when we submitted a Callable
task to an executor service, the service returned a Future
object. The submit()
methods execute tasks asynchronously and don’t block while we wait for the task to complete. We use the Future
object to retrieve the actual result from the Callable
task at a later stage when it suits us.
The Future
interface contains five methods:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
V get();
V get(long timeout, TimeUnit unit);
boolean isCancelled();
boolean isDone();
}
We use the isDone()
method to check if the task is completed. This gives us the opportunity to either do something else in our program while the task executes, or decide to wait at that point for the task to complete.
The two get()
methods get the value returned by the task. One waits (blocks) indefinitely for the task to complete; the other waits at most till the timeout expires, and then tries to retrieve the result. As can be imagined, the methods can throw a multitude of exceptions if things go wrong. Check the JSE API documentation for more details.
We can try to cancel the task with the cancel()
method. We can check to see whether the task was cancelled before it finished normally with the isCancelled()
method.
Remember that we can think of a Future
as a placeholder that stands in for a future result. We can submit a task to an executor service, and largely forget about it until we need the result of the task. At that point, we can query the Future
with future.isDone()
to find out whether the task is done. If it’s done, we can get the result by calling future.get()
. If not, we can either get busy with something else, or we can block at that point to wait for the task to complete.
Here’s a snippet of code from last week to remind you:
Callable<Integer> task = () -> {
try {
TimeUnit.SECONDS.sleep(2);
return 42;
}
catch (InterruptedException e) {
throw new IllegalStateException("Task interrupted!", e);
}
};
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(task);
System.out.println("Future done? " + future.isDone());
Integer result = future.get(); // blocks here until the Future is done
System.out.println("Future done? " + future.isDone());
System.out.println("Result of future = " + result);
Here we submitted the Callable
task to an ExecutorService
. If we immediately check whether the Future
has finished, isDone()
will return false
because the task has a delay in it. Calling the get()
method blocks the current thread and waits until the task has finished before returning the result. At this stage, calling isDone()
now returns true
.
Timeouts
A call to future.get()
will block until the Callable
task has finished or been terminated. In the worst case scenario, a Callable
can run forever. This would make our application unresponsive. This can be fixed by passing a timeout to the future.get()
method:
ExecutorService executor = Executors.newFixedThreadPool(1);
Callable<Integer> task = () -> {
try {
TimeUnit.SECONDS.sleep(3); // sleep for 3 seconds
return 42;
}
catch (InterruptedException e) {
throw new IllegalStateException("Task interrupted!", e);
}
};
Future<Integer> future = executor.submit(task);
future.get(1, TimeUnit.SECONDS); // time out after 1 second
Executing this code will result in a TimeoutException
:
Exception in thread "main" java.util.concurrent.TimeoutException
at java.util.concurrent.FutureTask.get(FutureTask.java:205)
at ConcurrencyTest5.main(ConcurrencyTest5.java:34)
The exception was thrown because a one second maximum wait time was passed to future.get()
, but the Callable
delayed for three seconds before returning the result.
Ending Off
In the next post we’ll look at executing scheduled tasks using the original Timer
and TimerTask
classes from Java 1.3, as well as with the more modern ScheduledExecutorService
.
Another thing we’ll look at next week is shutting down the executor service correctly. If we run the code in this and last week’s post, we’ll find that the program hangs and doesn’t terminate. We have to press Ctrl-C on Windows/Linux to terminate the JVM.
Are you on your way to becoming an expert on Java threads? Has this series helped you? Please share your comments and questions.
Until then, stay safe and keep learning!