Authors: Vivek Lakshmanan
Concurrency is the ability to run several programs or several parts of a program out-of-order, in an interleaved fashion. Simply put, if a program is running concurrently, the processor will execute one part of the program, pause it, execute another part and repeat.
As such, Java Concurrency enables you to perform tasks using multi-threading in your code and therefore:
Do note that Concurrency often gets confused with Parallelism which is a different property altogether. Parallelism is where parts of the program are executed at the same time, for example, on a multi-core processor. This StackOverflow post explains this in much greater detail.
There are many tutorials that cover Java Concurrency in-depth, such as the Java tutorial by Oracle. Instead, this chapter will provide an overview and things to take note of.
First off, a process is simply a program in execution. It contains at least one thread and has it's own memory space.
A thread:
There are two ways to create a thread in Java:
Thread
classRunnable
interfaceAfter which, override the run()
method which contains the code that will be executed when the thread starts.
Depending on how you created the Thread
you can either create the class that extends the Thread
class or pass the class that implements the Runnable
interface into the Thread
constructor and then start it.
When deciding which method to use to create a Thread
, it is always advisable to implement the Runnable
interface as this results in composition which will allow your code to be more loosely coupled as compared to inheritance. Furthermore, you can extend another class if need be. Shown below are the two ways to create a Thread
.
Extending the Thread
class:
public class AnotherThread extends Thread {
@Override
public void run() {
System.out.println("This class extends the Thread class");
}
}
Implemeting the Runnable
interface:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("This class implements the Runnable interface");
}
}
Do note that this is only for illustration and can be simplified using lambdas.
After creating and starting threads, you can carry out operations on them. There are several such operations you can use to manipulate threads:
try {
Thread.sleep(1000);
} catch (InterruptedException e){
// handle interruption
}
try {
thread2.join();
} catch (InterruptedException e) {
// handle interruption
}
Since threads share the resources of the process they exist in, there will inevitably be conflicts when using shared resources due to the unpredictable nature of threads. When threads try to write to the same resource, thread interference occurs. To illustrate this problem, here's a sequence of execution for two threads, A and B, that increment and decrement a counter variable respectively:
Without thread interference, the expected value of the counter variable would be 0, since one thread increments it while the other decrements it. But with thread interference, the value of counter is simply the value written by the last thread. This is due to the unpredictable nature of threads as there is no way to know when the operating system switches between the two threads.
To solve this issue of interference, the keyword synchronized
is used to ensure that method or block of code can only be accessed by one thread at a time. This is done through the use of the intrinsic lock system, a mechanism put in place by Java to control access to a shared resource. Simply put, each object has it's own intrinsic lock which:
This can be illustrated by the following image where once Thread 2 (T2) acquires the lock for the synchronized block of code, the other two threads (T1 and T3) must wait for the synchronized block of code to release it's lock once T2 to complete its execution:
For a deeper look, see the Java Synchronisation section.
While it is easy to create one or two threads and run them, it becomes a problem when your application requires creating 20 or 30 threads for running tasks concurrently. This is where the Executors
class comes in. It helps you with:
Thread Creation
It provides various methods for creating threads, more specifically a pool of threads, to run tasks concurrently.
Thread Management
It manages the life cycle of the threads in the thread pool. You don’t need to worry about whether the threads in the thread pool are active, busy or dead before submitting a task for execution.
Task submission and execution
It provides methods for submitting tasks for execution in the thread pool, and also allows you to decide when the tasks will be executed. For example, a task can be submitted to be executed instantly, scheduled to be executed later or even executed periodically. Tasks are submitted to a thread pool via an internal queue called the Blocking Queue
. If there are more tasks than the number of active threads, they are queued until a thread becomes available. New tasks are rejected if the blocking queue is full.
The Executors
class provides convenient factory methods for creating the ExecutorService
class. This class manages the lifecycle of tasks in various ways by assigning a ThreadPool
to it. Different thread pools manage tasks in different ways along with their own advantages and disadvantages. Some of these include:
CachedThreadPool
- Creates new threads as they are needed. This would prove useful for short-lived tasks but otherwise would be resource intensive as it would create too many threads.
FixedThreadPool
- A thread pool with a fixed number of threads. If all threads are active when a new task is submitted, they will be queued until a thread becomes available. This can come in handy when you know exactly how many threads you need, though that may be tricky by itself.
ThreadPoolExecutor
- This thread pool implementation adds the ability to configure parameters such as the maximum number of threads it can hold and how long to keep extra threads alive.
Shown below is an image to illustrate how ExecutorService
and ThreadPools
are connected (Do note that the Executor
is not in the image as it creates the ExecutorService
):
A simple example of using the Executors
class is shown below where after passing in the task to be executed, it is automatically managed by the ExecutorService
. For a more detailed look at the workflow of the ExecutorService
, see this in-depth tutorial.
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.execute(() -> {
System.out.println("Hello from: " + Thread.currentThread().getName());
});
executor.shutdown(); // Remember to shutdown the thread.
And the corresponding output would be Hello from: pool-1-thread-1
.
As the saying goes, there is no free lunch. While concurrency provides great benefits as mentioned above, it does come with several issues such as:
Singleton
design pattern:Concurrent implementation where you have to ensure that thread interference does not happen
public class Singleton{
private static Singleton singleton;
// Create a lock so only one thread can access this object at a time.
private static final Lock lock = new ReentrantLock();
private Singleton() {
//...
}
public static Singleton getSingleton() {
// This thread has acquired this object, so lock to ensure other threads don't interfere.
lock.lock();
try {
if (singleton == null) {
singleton = new Singleton();
}
} finally {
// Release lock once you're done so others can access this object.
lock.unlock();
}
return singleton;
}
}
Vs the usual implementation
public class Singleton {
private static Singleton singleton;
private Singleton() {
//...
}
public static Singleton getSingleton() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
Harder debugging and testing process
The unpredictable nature of threads result in errors that can be hard to detect, reproduce and fix as these errors don't crop up consistently like normal errors do.
Context switching overhead
When a CPU switches from executing one thread to executing another, the CPU needs to save the state of the current thread, and load the state of the next thread to execute, making this process of context switching very expensive.
The following resources are the many in-depth tutorials that will help you get a better grasp of concurrency in Java.
The following resources are interesting reads for a deeper understanding.