Skip to main content

Java Multithreading & Concurrency

Multithreading is a powerful feature in Java that allows concurrent execution of multiple parts of a program for maximum CPU utilization.

Understanding Threads

What is a Thread?

A thread is a lightweight process, the smallest unit of processing that can be scheduled by an operating system. Java supports multithreading through the java.lang.Thread class.
Program vs Process vs Thread:
  • Program: Static code (set of instructions)
  • Process: Running instance of a program with its own memory space
  • Thread: Lightweight sub-process within a process, shares memory

Thread Lifecycle

  1. NEW: Thread created but not started
  2. RUNNABLE: Thread ready to run or running
  3. BLOCKED: Waiting for monitor lock
  4. WAITING: Waiting indefinitely for another thread
  5. TIMED_WAITING: Waiting for specified time
  6. TERMINATED: Thread completed execution

Creating Threads

Method 1: Extending Thread Class

public class MyThread extends Thread {
    private String threadName;
    
    public MyThread(String name) {
        this.threadName = name;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(threadName + ": " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// Usage
public class ThreadDemo {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread("Thread-1");
        MyThread thread2 = new MyThread("Thread-2");
        
        thread1.start();
        thread2.start();
    }
}
Implementing Runnable is preferred because it allows your class to extend another class and promotes better design.
public class MyRunnable implements Runnable {
    private String taskName;
    
    public MyRunnable(String name) {
        this.taskName = name;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(taskName + ": " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

// Usage
public class RunnableDemo {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyRunnable("Task-1"));
        Thread thread2 = new Thread(new MyRunnable("Task-2"));
        
        thread1.start();
        thread2.start();
    }
}

Method 3: Using Lambda Expressions (Java 8+)

public class LambdaThreadDemo {
    public static void main(String[] args) {
        // Using lambda expression
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Lambda Thread: " + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        
        thread.start();
    }
}

Thread Synchronization

The Problem: Race Conditions

A race condition occurs when multiple threads access shared data concurrently, leading to unpredictable results.
public class Counter {
    private int count = 0;
    
    // UNSAFE - Race condition
    public void increment() {
        count++;  // Not atomic: read-modify-write
    }
    
    public int getCount() {
        return count;
    }
}

// Problem demonstration
public class RaceConditionDemo {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) counter.increment();
        });
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) counter.increment();
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println("Count: " + counter.getCount());
        // Expected: 2000, Actual: Often less due to race condition
    }
}

Solution 1: Synchronized Methods

public class SynchronizedCounter {
    private int count = 0;
    
    // SAFE - synchronized method
    public synchronized void increment() {
        count++;
    }
    
    public synchronized int getCount() {
        return count;
    }
}

Solution 2: Synchronized Blocks

Synchronized blocks provide finer-grained control over synchronization, improving performance by locking only critical sections.
public class SynchronizedBlockExample {
    private int count = 0;
    private Object lock = new Object();
    
    public void increment() {
        synchronized (lock) {
            count++;
        }
    }
    
    // Synchronized on this object
    public void decrement() {
        synchronized (this) {
            count--;
        }
    }
}

Solution 3: Locks (java.util.concurrent.locks)

import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
    private int count = 0;
    private ReentrantLock lock = new ReentrantLock();
    
    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();  // Always unlock in finally
        }
    }
    
    public void safeIncrement() {
        if (lock.tryLock()) {  // Non-blocking attempt
            try {
                count++;
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("Could not acquire lock");
        }
    }
}

Thread Communication

wait(), notify(), and notifyAll()

public class ProducerConsumer {
    private List<Integer> queue = new LinkedList<>();
    private int capacity = 5;
    
    public void produce() throws InterruptedException {
        int value = 0;
        while (true) {
            synchronized (this) {
                while (queue.size() == capacity) {
                    wait();  // Wait if queue is full
                }
                
                System.out.println("Produced: " + value);
                queue.add(value++);
                notify();  // Notify consumer
                Thread.sleep(1000);
            }
        }
    }
    
    public void consume() throws InterruptedException {
        while (true) {
            synchronized (this) {
                while (queue.isEmpty()) {
                    wait();  // Wait if queue is empty
                }
                
                int value = queue.remove(0);
                System.out.println("Consumed: " + value);
                notify();  // Notify producer
                Thread.sleep(1000);
            }
        }
    }
}

Executor Framework

Thread Pools

Creating threads is expensive. Use thread pools to reuse threads and improve performance.
import java.util.concurrent.*;

public class FixedThreadPoolExample {
    public static void main(String[] args) {
        // Create a pool with 3 threads
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        // Submit tasks
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + 
                    " executed by " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        
        executor.shutdown();
    }
}

Callable and Future

import java.util.concurrent.*;

public class CallableExample {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        
        // Callable returns a result
        Callable<Integer> task = () -> {
            Thread.sleep(2000);
            return 42;
        };
        
        // Submit callable and get Future
        Future<Integer> future = executor.submit(task);
        
        System.out.println("Task submitted");
        
        // Do other work...
        
        // Get result (blocks if not ready)
        Integer result = future.get();
        System.out.println("Result: " + result);
        
        executor.shutdown();
    }
}

Concurrent Collections

import java.util.concurrent.ConcurrentHashMap;

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

// Thread-safe operations
map.put("key1", 1);
map.putIfAbsent("key2", 2);
map.computeIfAbsent("key3", k -> 3);

// Atomic operations
map.compute("key1", (k, v) -> v == null ? 1 : v + 1);

Atomic Classes

import java.util.concurrent.atomic.*;

public class AtomicExample {
    private AtomicInteger counter = new AtomicInteger(0);
    private AtomicReference<String> atomicRef = new AtomicReference<>("initial");
    
    public void increment() {
        counter.incrementAndGet();  // Atomic increment
    }
    
    public void compareAndSet() {
        atomicRef.compareAndSet("initial", "updated");
    }
    
    public int addAndGet(int delta) {
        return counter.addAndGet(delta);
    }
}

Common Concurrency Patterns

Producer-Consumer with BlockingQueue

import java.util.concurrent.*;

public class ProducerConsumerPattern {
    private static BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10);
    
    static class Producer implements Runnable {
        public void run() {
            try {
                for (int i = 0; i < 100; i++) {
                    queue.put(i);
                    System.out.println("Produced: " + i);
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
    
    static class Consumer implements Runnable {
        public void run() {
            try {
                while (true) {
                    Integer item = queue.take();
                    System.out.println("Consumed: " + item);
                    Thread.sleep(200);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
    
    public static void main(String[] args) {
        new Thread(new Producer()).start();
        new Thread(new Consumer()).start();
    }
}

Thread-Safe Singleton

public class EagerSingleton {
    private static final EagerSingleton INSTANCE = new EagerSingleton();
    
    private EagerSingleton() {}
    
    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

Best Practices

1

Avoid Shared Mutable State

  • Use immutable objects when possible
  • Minimize shared state between threads
  • Use thread-local variables for thread-specific data
2

Use High-Level Concurrency Utilities

  • Prefer ExecutorService over manual thread creation
  • Use concurrent collections over synchronized collections
  • Leverage atomic classes for simple operations
3

Handle InterruptedException Properly

try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    // Restore interrupt status
    Thread.currentThread().interrupt();
    // Handle or propagate
}
4

Avoid Deadlocks

  • Always acquire locks in the same order
  • Use timeouts with tryLock()
  • Keep synchronized blocks small
5

Document Thread Safety

  • Clearly document thread-safety guarantees
  • Use annotations: @ThreadSafe, @NotThreadSafe

Common Pitfalls

Avoid these common concurrency mistakes:
// WRONG: Not thread-safe
private int count = 0;
public void increment() {
    count++;  // Race condition!
}

// CORRECT: Synchronized
private int count = 0;
public synchronized void increment() {
    count++;
}
// WRONG: Potential deadlock
synchronized (lock1) {
    synchronized (lock2) {
        // Critical section
    }
}
// Another thread:
synchronized (lock2) {
    synchronized (lock1) {  // Deadlock!
        // Critical section
    }
}
// WRONG: May not work correctly
private boolean running = true;

// CORRECT: Use volatile
private volatile boolean running = true;

Java Fundamentals

Learn Java basics

JVM Internals

Understand JVM architecture

Spring Framework

Enterprise development

Build docs developers (and LLMs) love