Background
You know there are 2 ways to create your threads and run it asynchronously from main thread. You can either -- extend Thread class or
- implement Runnable interface and pass it to a thread.
public static void main(String[] args) { Thread t1 = new Thread(() -> System.out.println("My ThreadId : " + Thread.currentThread().getId())); Thread t2 = new Thread(() -> System.out.println("My ThreadId : " + Thread.currentThread().getId())); Thread t3 = new Thread(() -> System.out.println("My ThreadId : " + Thread.currentThread().getId())); t1.start(); t2.start(); t3.start(); System.out.println("Main ThreadId : " + Thread.currentThread().getId()); }
One of the possible outputs is -
My ThreadId : 10
My ThreadId : 12
Main ThreadId : 1
My ThreadId : 11
Obviously, we cannot determine for sure the order of execution of threads as it is dependent on OS level thread scheduler. We will not get into that. Important points to note here is how we created and started a thread, used lambda expression for the runnable interface.
Runnable as we know is a functional interface and can be used in lambda expressions -
@FunctionalInterface public interface Runnable { public abstract void run(); }
So that's the normal way. But why manage thread starting/stopping, handing results if any on your own when Java provides you with a convenient way. And this is where ExecutorService comes into the picture.
In this post we will see what ExecutorService is, how we can use to create and run threads, various types of methods it supports etc.
Creating Threads with the ExecutorService
ExecutorService is an interface but it has multiple concrete implementations we can use. It is essentially a framework that creates and manages threads for you. It has other features like thread pooling and scheduling we will come to later.
Consider the following example -
public static void main(String[] args) { ExecutorService service = null; try { service = Executors.newSingleThreadExecutor(); service.execute(() -> System.out.println("My 1st ThreadId : " + Thread.currentThread().getId())); service.execute(() -> System.out.println("My 2nd ThreadId : " + Thread.currentThread().getId())); service.execute(() -> System.out.println("My 3rd ThreadId : " + Thread.currentThread().getId())); System.out.println("Main ThreadId : " + Thread.currentThread().getId()); } finally { if(service != null) service.shutdown(); } }
Here we have used newSingleThreadExecutor which is basically ExecutorService with a single thread. You send multiple runnable implementations to it and that single thread runs it sequentially. Since it is a single thread it's output will be sequential too (and hence predictable). However, you cannot predict the complete output of the above program as main threads output you cannot predict with respect to other threads as that runs independently.
One of the possible outputs of the above program -
My 1st ThreadId : 10
Main ThreadId : 1
My 2nd ThreadId : 10
My 3rd ThreadId : 10
Main ThreadId : 1
My 2nd ThreadId : 10
My 3rd ThreadId : 10
Another thing you might have noticed above is the shutdown method. You need to be very careful here. You need to call shutdown on executor service when you are done because executor service create non-daemon threads and your application will never shut down if those threads are not stopped. Once shutdown is called no more tasks are accepted by executor service but it continues running already accepted tasks. Below is it' life cycle -
NOTE: ExecutorService interface does not implement AutoCloseable, so you cannot use a try-with-resources statement.
Submitting tasks to executor service
As you must have seen in the code snippet above we use executor services execute() method to submit our runnables. But the problem with these is we never really know if the runnables have completed their operation. Fortunately, there is another method we can use called submit() - this also takes a Runnable as an argument but returns a Future object that can be used to determine if task is complete or not.
public static void main(String[] args) throws InterruptedException, ExecutionException { ExecutorService service = null; try { service = Executors.newSingleThreadExecutor(); Future<?> result = service.submit(() -> System.out.println("My 1st ThreadId : " + Thread.currentThread().getId())); while(!result.isDone()){ System.out.println("Executor task in progress"); Thread.sleep(100); } System.out.println("Executor task completed : " + result.isDone()); } finally { if(service != null) service.shutdown(); } }
Above prints -
Executor task in progress
My 1st ThreadId : 10
Executor task completed : true
In case you noticed Future object also has a get method. So if you are wondering what does it return then the answer is it depends. So far we have seen simply Runnable instance provided to submit method and since we know Runnable does not return anything (void return type) get on corresponding future will also return null. However, there is another interface called Callable similar to Runnable.
- Callable allows you to return a value.
- It also allows you to throw checked exceptions.
- And you can use it in submit/execute method of ExecutorService.
- And yes it's also a functional interface like Runnable.
@FunctionalInterface public interface Callable<V> { /** * Computes a result, or throws an exception if unable to do so. * * @return computed result * @throws Exception if unable to compute a result */ V call() throws Exception; }
The Callable interface was introduced as an alternative to the Runnable interface,
since it allows more details to be retrieved easily from the task after it is completed.
Sample example for Callable interface use -
public static void main(String[] args) throws InterruptedException, ExecutionException { ExecutorService service = null; try { service = Executors.newSingleThreadExecutor(); Future<String> result = service.submit(() -> "My 1st ThreadId : " + Thread.currentThread().getId()); while(!result.isDone()){ System.out.println("Executor task in progress"); Thread.sleep(100); } System.out.println("Executor task completed : " + result.get()); } finally { if(service != null) service.shutdown(); } }
Output -
Executor task in progress
Executor task completed: My 1st ThreadId : 10
NOTE: Callable functional interface is similar to Supplier functional interface. Both don't take any argument and return something. If Java compiler at any point finds this ambiguous then it will throw a compilation error. You need to typecast it to work.
Eg.
public static void useMe(Supplier<String> input) {} public static void useMe(Callable<String> input) {} public static void main(String[] args) { useMe(() -> {throw new IOException();}); // DOES NOT COMPILE }
The compilation will fail with following error -
The method useMe(Supplier<String>) is ambiguous for the type Java8Demo
Also, note Callable can throw checked exception while Supplier cannot. But Java does not check this as seen in above example.
NOTE: Until now we have only seen a single threaded executor service. But you can have a pool as well i.e executor service with a predefined number of threads in the pool.
For eg. ExecutorService executor = Executors.newFixedThreadPool(5);
NOTE: ExecutorService was introduced since Java 5.