Multithreading and Concurrency in Java – Complete Guide


Learn the concepts of multithreading and concurrency in Java, including thread creation, synchronization, executor framework, and concurrent collections for efficient parallel processing.

Java's multithreading and concurrency capabilities allow programs to execute multiple tasks in parallel, improving performance, scalability, and responsiveness.

1. Thread Basics

  1. A thread is the smallest unit of execution in a program.
  2. Java provides multithreading support to allow the execution of multiple threads in parallel.

Key Concepts:

  1. Each thread has its own program counter, stack, and local variables.
  2. Threads share global memory, so synchronization is required to manage shared resources.

2. Creating Threads (Thread Class, Runnable Interface)

  1. Thread Class: You can extend the Thread class to create a thread by overriding the run() method.
  2. Runnable Interface: You can implement the Runnable interface, which allows the class to be executed by a thread.

Using Thread Class


class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running using Thread class!");
}

public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}

Using Runnable Interface


class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread is running using Runnable interface!");
}

public static void main(String[] args) {
MyRunnable task = new MyRunnable();
Thread thread = new Thread(task);
thread.start();
}
}

Key Points:

  1. Thread.start() is used to initiate a thread.
  2. run() contains the task to be executed by the thread.

3. Thread Lifecycle

A thread goes through various stages during its execution:

  1. New: The thread is created but not started.
  2. Runnable: The thread is ready for execution and waiting for CPU time.
  3. Blocked: The thread is waiting to acquire resources or locks.
  4. Terminated: The thread has completed its execution.

Thread States Diagram:


New → Runnable → Blocked → Terminated

4. Thread Priority

  1. Threads in Java have a priority that determines the order in which they are scheduled.
  2. Priority values range from Thread.MIN_PRIORITY (1) to Thread.MAX_PRIORITY (10).

Example:


public class ThreadPriorityExample {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> System.out.println("Thread 1 is running"));
Thread thread2 = new Thread(() -> System.out.println("Thread 2 is running"));

thread1.setPriority(Thread.MAX_PRIORITY); // Highest priority
thread2.setPriority(Thread.MIN_PRIORITY); // Lowest priority

thread1.start();
thread2.start();
}
}

Key Points:

  1. Thread priority is managed using setPriority(int priority).
  2. Thread priority is not guaranteed; it depends on the underlying OS scheduler.

5. Synchronization

  1. Synchronization ensures that only one thread can access a resource at a time.
  2. This is critical when multiple threads share data (e.g., updating a counter or modifying a list).

Example – Synchronized Method:


class Counter {
private int count = 0;

// Synchronized method to ensure thread-safe increment
public synchronized void increment() {
count++;
}

public int getCount() {
return count;
}
}

public class Main {
public static void main(String[] args) {
Counter counter = new Counter();

Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});

Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});

thread1.start();
thread2.start();
}
}

Key Points:

  1. synchronized keyword ensures that only one thread can execute a method at a time.
  2. Synchronization prevents data inconsistency and race conditions.

6. Locks and Monitors

  1. Java uses monitors to allow only one thread to access a critical section of code.
  2. ReentrantLock is an alternative to synchronized methods/blocks, providing more flexibility.

import java.util.concurrent.locks.ReentrantLock;

class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();

public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}

public int getCount() {
return count;
}
}

Key Points:

  1. ReentrantLock provides explicit lock control.
  2. Use lock.lock() and lock.unlock() for manual locking and unlocking.

7. Deadlock, Livelock, Race Condition

  1. Deadlock: When two or more threads wait forever for each other to release resources.
  2. Livelock: Threads are actively trying to make progress, but due to resource contention, they never succeed.
  3. Race Condition: When the output depends on the timing of thread execution, resulting in inconsistent data.

Example of Deadlock:


class A {
synchronized void methodA(B b) {
b.last();
}

synchronized void last() {}
}

class B {
synchronized void methodB(A a) {
a.last();
}

synchronized void last() {}
}

public class DeadlockExample {
public static void main(String[] args) {
A a = new A();
B b = new B();

new Thread(() -> a.methodA(b)).start();
new Thread(() -> b.methodB(a)).start();
}
}

Key Points:

  1. Avoiding deadlock: Ensure a consistent ordering of resource acquisition.
  2. Use timeouts or tryLock() to handle deadlocks.

8. Executor Framework

  1. The Executor Framework provides a higher-level replacement for managing threads.
  2. It decouples task submission from thread management.

Example – Using ExecutorService:


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);

for (int i = 0; i < 5; i++) {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " is executing task");
});
}

executor.shutdown();
}
}

Key Points:

  1. ExecutorService manages a pool of threads, handling tasks efficiently.
  2. Use submit() for task execution and shutdown() to stop the executor.

9. Callable and Future

  1. Callable: Similar to Runnable, but it can return a result or throw an exception.
  2. Future: Represents the result of an asynchronous computation.

Example – Using Callable and Future:


import java.util.concurrent.*;

public class CallableExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(1);
Callable<Integer> task = () -> {
TimeUnit.SECONDS.sleep(2);
return 123;
};

Future<Integer> future = executor.submit(task);
System.out.println("Result: " + future.get()); // blocks until the result is available
executor.shutdown();
}
}

Key Points:

  1. Callable allows returning a result from a thread.
  2. Future.get() blocks until the result is available.

10. ThreadPoolExecutor

  1. ThreadPoolExecutor is a customizable thread pool that handles a large number of tasks efficiently by reusing threads.

Example:


ExecutorService executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>()
);

Key Points:

  1. ThreadPoolExecutor allows fine-grained control over thread pool configuration.

11. ScheduledExecutorService

  1. ScheduledExecutorService allows scheduling tasks with fixed-rate or fixed-delay execution.

Example – Scheduling a Task:


import java.util.concurrent.*;

public class ScheduledExecutorExample {
public static void main(String[] args) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> System.out.println("Task executed every 2 seconds"), 0, 2, TimeUnit.SECONDS);
}
}

12. Concurrent Collections (ConcurrentHashMap, CopyOnWriteArrayList)

  1. Java provides thread-safe collections that allow concurrent access without requiring manual synchronization.

Example – ConcurrentHashMap:


import java.util.concurrent.*;

public class ConcurrentMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("key1", "value1");
System.out.println(map.get("key1"));
}
}

Key Points:

  1. ConcurrentHashMap: Thread-safe without blocking for reading.
  2. CopyOnWriteArrayList: Allows thread-safe modifications of lists.