java.util.concurrent
package. This package provides a rich set of classes and interfaces that simplify the development of multi - threaded applications, allowing developers to write efficient and robust concurrent code. In this blog, we will take a deep dive into Java’s concurrent package, covering fundamental concepts, usage methods, common practices, and best practices.In Java, a thread is an independent path of execution within a program. Concurrency refers to the ability of a program to execute multiple threads simultaneously. Multiple threads can share resources such as memory, which can lead to issues if not properly managed. For example, two threads might try to modify the same variable at the same time, leading to inconsistent results.
A race condition occurs when the behavior of a program depends on the relative timing of events in different threads. To prevent race conditions, Java provides synchronization mechanisms. The synchronized
keyword can be used to ensure that only one thread can access a block of code or a method at a time.
public class SynchronizedExample {
private int counter = 0;
public synchronized void increment() {
counter++;
}
public int getCounter() {
return counter;
}
}
The Executor framework in Java provides a higher - level way to manage threads. It decouples task submission from task execution. The ExecutorService
interface is the core of the Executor framework, and it can be used to submit tasks for execution.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> {
System.out.println("Task 1 is running");
});
executor.submit(() -> {
System.out.println("Task 2 is running");
});
executor.shutdown();
}
}
As shown in the previous example, we can use the Executors
factory class to create different types of thread pools. For example, newFixedThreadPool
creates a thread pool with a fixed number of threads, newCachedThreadPool
creates a thread pool that can grow and shrink as needed, and newSingleThreadExecutor
creates a single - threaded executor.
Blocking queues are thread - safe queues that can block the calling thread if the queue is full (when adding an element) or empty (when removing an element). The ArrayBlockingQueue
and LinkedBlockingQueue
are two commonly used implementations.
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueExample {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
try {
queue.put(1);
Integer element = queue.take();
System.out.println("Retrieved element: " + element);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Atomic variables are classes in the java.util.concurrent.atomic
package that provide atomic operations. They are useful for implementing lock - free algorithms. For example, the AtomicInteger
class can be used to perform atomic increment and decrement operations.
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicVariableExample {
private static AtomicInteger atomicCounter = new AtomicInteger(0);
public static void main(String[] args) {
atomicCounter.incrementAndGet();
System.out.println("Atomic counter value: " + atomicCounter.get());
}
}
The producer - consumer pattern is a classic concurrent programming pattern where producers generate data and add it to a shared buffer, and consumers remove data from the buffer. Blocking queues are often used to implement this pattern.
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
class Producer implements Runnable {
private final BlockingQueue<Integer> queue;
public Producer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
for (int i = 0; i < 5; i++) {
queue.put(i);
System.out.println("Produced: " + i);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
class Consumer implements Runnable {
private final BlockingQueue<Integer> queue;
public Consumer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
while (true) {
Integer item = queue.take();
System.out.println("Consumed: " + item);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
Thread producerThread = new Thread(new Producer(queue));
Thread consumerThread = new Thread(new Consumer(queue));
producerThread.start();
consumerThread.start();
}
}
The Fork/Join framework is designed for parallel processing of tasks that can be divided into smaller subtasks. It uses a work - stealing algorithm to efficiently distribute the workload among threads.
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
class SumTask extends RecursiveTask<Integer> {
private static final int THRESHOLD = 10;
private final int[] array;
private final int start;
private final int end;
public SumTask(int[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
if (end - start <= THRESHOLD) {
int sum = 0;
for (int i = start; i < end; i++) {
sum += array[i];
}
return sum;
} else {
int mid = (start + end) / 2;
SumTask leftTask = new SumTask(array, start, mid);
SumTask rightTask = new SumTask(array, mid, end);
leftTask.fork();
int rightResult = rightTask.compute();
int leftResult = leftTask.join();
return leftResult + rightResult;
}
}
}
public class ForkJoinExample {
public static void main(String[] args) {
int[] array = new int[100];
for (int i = 0; i < 100; i++) {
array[i] = i;
}
ForkJoinPool pool = new ForkJoinPool();
SumTask task = new SumTask(array, 0, array.length);
int result = pool.invoke(task);
System.out.println("Sum: " + result);
}
}
The size of the thread pool should be carefully chosen based on the nature of the tasks and the available resources. For CPU - bound tasks, the number of threads should be close to the number of available CPU cores. For I/O - bound tasks, a larger number of threads can be used to keep the CPU busy while waiting for I/O operations to complete.
Deadlocks occur when two or more threads are blocked forever, each waiting for the other to release a resource. To avoid deadlocks, follow these rules:
Java’s concurrent package provides a rich set of tools and mechanisms for developing multi - threaded applications. By understanding the fundamental concepts, usage methods, common practices, and best practices, developers can write efficient and robust concurrent code. Whether it’s implementing simple producer - consumer patterns or performing complex parallel processing, the java.util.concurrent
package offers the necessary building blocks to achieve high - performance and responsive applications.