Java Concurrency Strategies
Introduction
A thread is similar to a lightweight OS process, except that it can access shared memory. Every thread has its own memory cache. Java runs in its own process, and threads in Java run within this process. Java has native support for multithreaded programming, which is one of many reasons why Java currently holds its status as one of the best overall programming languages.
Multithreaded programming is about speed and handling deadlocks (due to concurrent access) and safety issues (such as incorrect data resulting from threads reading old or inconsistent data).
Java provides several locking mechanisms that enforce exclusivity (ensuring only one thread accesses certain parts of the code at a time).
Let’s explore some strategies for concurrent programming in Java.
First, we need a sufficiently long task to demonstrate performance differences across strategies. For this purpose, I created a FibonaciCalculator
that generates an array of Fibonacci numbers. Generating a Fibonacci array is not inherently a CPU-intensive task, so I added Thread.sleep
between iterations to simulate a longer workload.
public class FibonaciCalculator {
public static final int LONG_TASK_SIMULATED_DELAY = 150;
public static int calculateFibonaciNumber(int n) {
int previous = 0;
int result = 1;
for (int i = 0; i <= n; i++) {
simulateCostlyTask();
int sum = result + previous;
previous = result;
result = sum;
}
return result;
}
private static void simulateCostlyTask() {
try {
Thread.sleep(LONG_TASK_SIMULATED_DELAY);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Single-threaded Strategy
First, we’ll examine a simple single-threaded model. This is a standard non-recursive implementation of the Fibonacci number algorithm in Java.
public class SingleThreadedFibonaciArrayConstructor extends FibonaciArrayConstructor{
protected int[] constructFibonaciNumbersArray(int size) {
int[] fibonaciNumbers = new int[size];
for (int i = 0; i < fibonaciNumbers.length; i++) {
fibonaciNumbers[i] = FibonaciCalculator.calculateFibonaciNumber(i);
}
return fibonaciNumbers;
}
}
Simple Multi-threaded Strategy
Our second strategy is a simple, brute-force multi-threaded implementation. In this example, we will use the fundamental concurrency mechanisms in Java: java.lang.Thread
. We will implement the Runnable
interface and create a new thread for each Runnable
instance (in our case, for each Fibonacci number calculation in the array). Since each thread accesses the currentIndex
field, we need to synchronize its modification (incrementation) and retrieval. To achieve this, we use the synchronized
keyword. Synchronization is the simplest way to lock a certain part of code. This keyword ensures that only a single thread can access the marked section of code.
public class SimpleMultiThreadedFibonaciArrayConstructor extends FibonaciArrayConstructor{
private int[] fibonaciNumbers;
private int currentIndex = 0;
protected int[] constructFibonaciNumbersArray(int size) {
fibonaciNumbers = new int[size];
List<Thread> threads = new ArrayList<Thread>();
for (int i = 0; i < size; i++) {
Runnable task = new MyRunnable();
Thread worker = new Thread(task);
worker.setName(String.valueOf(i));
worker.start();
threads.add(worker);
}
int running;
do {
running = 0;
for (Thread thread : threads) {
if (thread.isAlive()) {
running ++;
}
}
} while (running > 0);
return fibonaciNumbers;
}
private synchronized int getNextIndex() {
return currentIndex++;
}
private class MyRunnable implements Runnable {
public void run(){
int nextIndex = getNextIndex();
int calculatedFibonaci = FibonaciCalculator.calculateFibonaciNumber(nextIndex);
fibonaciNumbers[nextIndex] = calculatedFibonaci;
}
}
}
Thread Pool Strategy
In the previous example, we used a maximum number of threads (equal to the array size) for calculating the Fibonacci array. This approach can have disadvantages, as it may cause performance overhead due to creating and switching between numerous threads. The java.util.concurrent
package offers improved support for concurrent programming. When using this package, we create a pool of worker threads. A thread pool contains a work queue that holds tasks waiting to be executed. This thread pool can be described as a collection of Runnable
tasks (the work queue) and a collection of running threads. These threads are constantly running and check the work queue for new tasks. If new work is available, they execute the Runnable
. The Executor
framework itself provides methods, such as execute(Runnable r)
, to add Runnable
tasks to the work queue. The Executor
framework provides example implementations of the java.util.concurrent.Executor
interface, such as Executors.newFixedThreadPool(int n)
, which creates n
worker threads.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadPooledMultiThreadedFibonaciArrayConstructor extends FibonaciArrayConstructor {
private int[] fibonaciNumbers;
private AtomicInteger currentIndex = new AtomicInteger(0);
protected int[] constructFibonaciNumbersArray(int size) {
fibonaciNumbers = new int[size];
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < size; i++) {
Runnable worker = new MyRunnable();
executor.execute(worker);
}
executor.shutdown();
while (!executor.isTerminated()) {
// Wait for all tasks to complete
}
return fibonaciNumbers;
}
private class MyRunnable implements Runnable {
public void run(){
int nextIndex = currentIndex.getAndAdd(1);
int calculatedFibonaci = FibonaciCalculator.calculateFibonaciNumber(nextIndex);
fibonaciNumbers[nextIndex] = calculatedFibonaci;
}
}
}
Note that we are no longer using a synchronized method; instead, we are using the AtomicInteger
type. This is part of Java’s non-blocking algorithm support, which is based on low-level atomic hardware primitives. The getAndAdd(1)
method retrieves the current value and increments it by one as a single, atomic operation, eliminating the need for explicit synchronization and the danger of dirty reads or deadlocks.
Future and Callables Strategy
If you expect your threads to return a computed result, you can use java.util.concurrent.Callable
. Callable
tasks allow values to be returned after completion. If you submit a Callable
to an Executor
, the framework returns a java.util.concurrent.Future
. This Future
can be used to check the status of a Callable
and to retrieve its result.
In both previous examples, we were compelled to use mechanisms like non-blocking algorithm support (AtomicInteger
) or method synchronization due to the shared currentIndex
variable. Threads also share the Fibonacci array, but there are no synchronization issues with it as each thread writes to a unique index. Wouldn’t it be better if we could remove the dependency on this currentIndex
variable? Yes, of course it would. We can achieve this by creating defensive copies for our thread execution algorithm.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class FeaturesAndCallablesMultiThreadedFibonaciArrayConstructor extends FibonaciArrayConstructor{
protected int[] constructFibonaciNumbersArray(int size) {
int[] fibonaciNumbers = new int[size];
ExecutorService executor = Executors.newFixedThreadPool(5);
List<Future<Integer>> list = new ArrayList<Future<Integer>>();
for (int i = 0; i < size; i++) {
Callable<Integer> worker = new MyCallable(i);
Future<Integer> submit = executor.submit(worker);
list.add(submit);
}
for (int i = 0; i < list.size(); i++) {
try {
Future<Integer> future = list.get(i);
int futureResult = future.get();
fibonaciNumbers[i] = futureResult;
} catch (Exception e) {
e.printStackTrace();
}
}
executor.shutdown();
return fibonaciNumbers;
}
private class MyCallable implements Callable<Integer> {
int currentIndex;
public MyCallable(int currentIndex) {
this.currentIndex = currentIndex;
}
public Integer call() throws Exception {
return FibonaciCalculator.calculateFibonaciNumber(currentIndex);
}
}
}
Testing and Conclusion
Test Result Summary:
- SingleThreadedFibonaciArrayConstructor: 48 seconds (1 thread)
- SimpleMultiThreadedFibonaciArrayConstructor: 3 seconds (25 threads)
- ThreadPooledMultiThreadedFibonaciArrayConstructor: 11 seconds (5 threads)
- FeaturesAndCallablesMultiThreadedFibonaciArrayConstructor: 11 seconds (5 threads)
When running all these implementations through tests, we observed the following results. Naturally, it can be seen that more threads generally lead to faster execution. In the third and fourth implementations, we used 5 threads, as opposed to the second implementation where we used 25 threads.
Java 7 Fork-Join Mechanism
Java 7 introduced a new parallel mechanism for compute-intensive tasks: the Fork/Join Framework. The Fork/Join Framework allows you to distribute a given task among several workers and then wait for the results. However, as I haven’t installed Java 7 yet (and many others haven’t), it will not be described in this post.
The complete implementation (with tests) can be found here. ```
Enjoy Reading This Article?
Here are some more articles you might like to read next: