By Shubham Aggarwal | 1/19/2017 | General |Intermediate

Threads and Executors in Java

Threads and Executors in Java

This article introduces you to Threads and Executors in Java, how are they made, of what use they are and what purpose do they actually achieve. Finally, We’ll also have a look at when not to use them which is a major concern.

 

This article will teach you to concurrent programming in Java 8 with easy to go program snippets. In the next 15 min you will learn how to execute code in parallel via threads, tasks and executor services.

 

The Concurrency API was first introduced in Java 5 and then continuously improved with every new Java version. The majority of concepts shown in this article will work in older versions of Java as well. However my code samples focus on Java 8 and make heavy use of lambda expressions and other new features. If you're not yet familiar with lambdas I recommend reading my Java 8 Tutorial first.

Threads and Runnables

All modern operating systems support concurrency via processes and threads. Processes are instances of programs which typically run independent to each other, e.g. if you start a java program, the operating system will start a process with a main thread. Inside those processes we can utilize threads to execute code concurrently, so we use all the available cores of the CPU.

 

Before starting a new thread you have to specify the code to be executed by this thread, often called the task. This is done by implementing a Runnable - a functional interface defining a single void no-args method run() as shown in the below example:

 

 

 Runnable task = () -> {
   String threadName = Thread.currentThread().getName();
   System.out.println("Hello " + threadName);
};

task.run();

Thread thread = new Thread(task);
thread.start();

System.out.println("Done!");

Since Runnable is a functional interface we can utilize Java 8 lambda expressions to print the current thread’s name to the console. First we execute the runnable directly on the main thread before starting a new thread.

The result on the console might look like this:

 

 

Hello main
Hello Thread-0
Done!

Or:

Hello main
Done!
Hello Thread-0

Due to concurrent execution, we cannot predict if the runnable will be invoked before or after printing 'done with thread'. The order is non-deterministic, thus making concurrent programming a complex task in larger applications.

 

Now let's take a deeper look at one of the most important parts of the Concurrency API - the executor services.

Executors

The Concurrency API introduces the concept of an ExecutorService as a higher level replacement for working with threads directly. Executors are capable of running asynchronous tasks and typically manage a pool of threads, so we don't have to create new threads manually. All threads of the internal pool will be reused under the hood for revenant tasks, so we can run as many concurrent tasks as we want throughout the lifecycle of our application with a single executor service.

 

This is how the first thread-example looks like using executors:

 

 

ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
   String threadName = Thread.currentThread().getName();
   System.out.println("I am " + threadName);
});

The class ExecutorService comes with helping factory methods for creating multiple types of services. In this sample we use an executor with a thread pool of size one.

 

The result looks similar to the above example but while running the code we'll notice a significant difference: the java process never ends! Executors have to be stopped explicitly - otherwise they keep waiting and listening for new tasks.

 

 

>>> Executors have to be stopped explicitly.

An ExecutorService provides 2 methods to stop an executor: shutdown() waits for currently running tasks to finish while shutdownNow() interrupts all running tasks and shut the executor down as soon as it is called.

 

A code example showing how to stop an executor is shown below:

 

 

try {
   System.out.println("attempt to shutdown executor");
   executor.shutdown();
   executor.awaitTermination(5, TimeUnit.SECONDS);
}
catch (InterruptedException e) {
   System.err.println("tasks interrupted");
}
finally {
   if (!executor.isTerminated()) {
       System.err.println("cancel non-finished tasks");
   }
   executor.shutdownNow();
   System.out.println("shutdown finished");
}

The executor will shut down silently by waiting for some time.

 

Callables and Futures

Apart from Runnable, executors support another type of task named Callable. Callables are functional interfaces just like runnables but they also return a value.

 

Following code snippet defines a callable returning an int right after sleeping for a second:

 

Callable<Integer> task = () -> {
   try {
       TimeUnit.SECONDS.sleep(1);
       return 123;
   }
   catch (InterruptedException e) {
       throw new IllegalStateException("task interrupted", e);
   }
};

 

Similar to runnables, Callables can be submitted to executor services. But what about the result they return? Since submit() won’t wait until the task is complete, the executor service cannot return the result of the callable directly. Instead the executor returns a special result of type Future which can be used to obtain the original result at a later point in time.

ExecutorService executor = Executors.newFixedThreadPool(1);
Future<Integer> future = executor.submit(task);

System.out.println("future done? " + future.isDone());

Integer result = future.get();

System.out.println("future done? " + future.isDone());
System.out.print("result: " + result);


After submitting the callable to the executor we first check if the future has already been finished execution via isDone(). We can be pretty sure this isn't the case since the above callable sleeps for one second before returning the integer.

Calling the method get() blocks the current thread and waits until the callable completes before returning the original result 123. Now the future is finally done and we see the below result on the terminal:

 

 

future done? false
future done? true
result: 123

>>> Futures are tightly coupled to the underlying executor services.

Important to note that every non-terminated future will throw exceptions if you shutdown the executor:

 

executor.shutdownNow();
future.get();

 

Worth noticing that the creation of the executor differs slightly from the previous sample. We now use newFixedThreadPool(1) to create an executor service backed by a thread-pool of size one. This is similar to newSingleThreadExecutor() but we can later increase the pool size by simply passing a value larger than one.

Timeouts

Any call to future.get() will block and wait until the underlying callable has been terminated. In the worst case a callable runs forever - thus making your application unresponsive. You can simply counteract those scenarios by passing a timeout:

ExecutorService executor = Executors.newFixedThreadPool(1);

Future<Integer> future = executor.submit(() -> {
   try {
       TimeUnit.SECONDS.sleep(2);
       return 123;
   }
   catch (InterruptedException e) {
       throw new IllegalStateException("task interrupted", e);
   }
});

future.get(1, TimeUnit.SECONDS);

 

Executing the above code results in a TimeoutException:

Exception in thread "main" java.util.concurrent.TimeoutException
   at java.util.concurrent.FutureTask.get(FutureTask.java:205)

You might already have guessed why this exception is thrown: We specified a maximum wait time of one second but the callable actually needs two seconds before returning the result.

Scheduled Executors

In order to periodically run common tasks multiple times, scheduled thread pools comes to our rescue.

 

A ScheduledExecutorService is capable of scheduling tasks to run either periodically or once after a certain amount of time has elapsed.

This code sample schedules a task to run after an initial delay of three seconds has passed:

 

ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);

Runnable task = () -> System.out.println("Scheduling: " + System.nanoTime());
ScheduledFuture<?> future = executor.schedule(task, 3, TimeUnit.SECONDS);

TimeUnit.MILLISECONDS.sleep(1337);

long remainingDelay = future.getDelay(TimeUnit.MILLISECONDS);
System.out.printf("Remaining Delay: %sms", remainingDelay);

 

Scheduling a task produces a specialized future of type ScheduledFuture which - in addition to Future - provides the method getDelay() to retrieve the remaining delay. After this delay has elapsed the task will be executed concurrently.

 

In order to schedule tasks to be executed periodically, executors provide the two methods scheduleAtFixedRate() and scheduleWithFixedDelay(). The first method is capable of executing tasks with a fixed time rate, e.g. once every second as demonstrated in this example:

ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);

Runnable task = () -> System.out.println("Scheduling: " + System.nanoTime());

int initialDelay = 0;
int period = 1;
executor.scheduleAtFixedRate(task, initialDelay, period, TimeUnit.SECONDS);



Additionally this method accepts an initial delay which describes the leading wait time before the task will be executed for the first time.

Please keep in mind that scheduleAtFixedRate() doesn't take into account the actual duration of the task. So if you specify a period of one second but the task needs 2 seconds to be executed then the thread pool will be working to capacity very soon.

In that case you should consider using scheduleWithFixedDelay() instead. This method works just like the counterpart described above. The difference is that the wait time period applies between the end of a task and the start of the next task. For example:

 

ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);

Runnable task = () -> {
   try {
       TimeUnit.SECONDS.sleep(2);
       System.out.println("Scheduling: " + System.nanoTime());
   }
   catch (InterruptedException e) {
       System.err.println("task interrupted");
   }
};

executor.scheduleWithFixedDelay(task, 0, 1, TimeUnit.SECONDS);

 

This example schedules a task with a fixed delay of one second between the end of an execution and the start of the next execution. The initial delay is zero and the tasks duration is two seconds. So we end up with an execution interval of 0s, 3s, 6s, 9s and so on. As you can see scheduleWithFixedDelay() is handy if you cannot predict the duration of the scheduled tasks.

Conclusion

This article introduced you to Threads and Executors in Java, how are they made, of what use they are and what purpose do they actually achieve. Finally, We also had a look at callables, Timeouts and Scheduled executors which help us to create parallel and concurrent programs.


Let’s put all the above to use in creating better apps.

By Shubham Aggarwal | 1/19/2017 | General

{{CommentsModel.TotalCount}} Comments

Your Comment

{{CommentsModel.Message}}

Recent Stories

Top DiscoverSDK Experts

User photo
3355
Ashton Torrence
Web and Windows developer
GUI | Web and 11 more
View Profile
User photo
3220
Mendy Bennett
Experienced with Ad network & Ad servers.
Mobile | Ad Networks and 1 more
View Profile
User photo
3060
Karen Fitzgerald
7 years in Cross-Platform development.
Mobile | Cross Platform Frameworks
View Profile
Show All
X

Compare Products

Select up to three two products to compare by clicking on the compare icon () of each product.

{{compareToolModel.Error}}

Now comparing:

{{product.ProductName | createSubstring:25}} X
Compare Now