You are viewing a preview of this lesson. Sign in to start learning
Back to Java

Concurrency & Multithreading

Build high-performance parallel applications

Java Concurrency & Multithreading

Master concurrent programming in Java with free flashcards and hands-on code examples. This lesson covers thread creation, synchronization, the Java Memory Model, thread pools, and common concurrency utilitiesโ€”essential concepts for building high-performance, thread-safe applications.

Welcome to Concurrent Java! ๐Ÿ’ปโšก

In today's world of multi-core processors, understanding concurrency is no longer optionalโ€”it's essential. Java provides robust tools for writing programs that do multiple things at once, from handling thousands of web requests to processing data in parallel. While concurrency can seem daunting at first, mastering it will dramatically expand what you can build.

What You'll Learn:

  • Creating and managing threads
  • Synchronization and thread safety
  • The Java Memory Model and visibility
  • Concurrent collections and atomic operations
  • Thread pools and the Executor framework
  • Common pitfalls and how to avoid them

Core Concepts ๐Ÿง 

1. Threads: The Basics ๐ŸŽฏ

A thread is the smallest unit of execution within a process. Java programs start with one thread (the main thread), but you can create more to perform tasks concurrently.

Two ways to create threads:

// Method 1: Extend Thread class
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread running: " + Thread.currentThread().getName());
    }
}

// Method 2: Implement Runnable (preferred)
class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("Task running: " + Thread.currentThread().getName());
    }
}

// Usage
Thread t1 = new MyThread();
t1.start(); // Don't call run() directly!

Thread t2 = new Thread(new MyTask());
t2.start();

// Modern lambda approach
Thread t3 = new Thread(() -> {
    System.out.println("Lambda thread: " + Thread.currentThread().getName());
});
t3.start();

๐Ÿ’ก Why prefer Runnable? Java doesn't support multiple inheritance. If your class extends Thread, it can't extend anything else. Implementing Runnable keeps your options open.

THREAD LIFECYCLE

  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”  start()   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚ NEW  โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’โ”‚ RUNNABLEโ”‚โ†โ”€โ”€โ”
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜            โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜   โ”‚
                           โ”‚        โ”‚
                   runs on CPU       โ”‚
                           โ”‚        โ”‚
                           โ†“        โ”‚
                      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
                      โ”‚ RUNNING โ”‚โ”€โ”€โ”€โ”˜
                      โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜
                           โ”‚
          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
          โ”‚                โ”‚                โ”‚
      sleep()          wait()           run()
      join()           I/O             completes
          โ”‚                โ”‚                โ”‚
          โ†“                โ†“                โ†“
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    โ”‚ BLOCKED/ โ”‚     โ”‚ WAITING โ”‚     โ”‚ TERMINATED โ”‚
    โ”‚  TIMED   โ”‚     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    โ”‚ WAITING  โ”‚          โ†‘
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     notify()
          โ”‚           notifyAll()
          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

2. Race Conditions & Synchronization ๐Ÿ”’

A race condition occurs when multiple threads access shared data simultaneously, and at least one modifies it. The outcome depends on unpredictable timing.

// UNSAFE: Race condition!
class Counter {
    private int count = 0;
    
    public void increment() {
        count++; // NOT atomic! Read-modify-write
    }
    
    public int getCount() {
        return count;
    }
}

// Multiple threads calling increment() will lose updates!

๐Ÿค” Did you know? count++ compiles to three separate bytecode instructions: load, add, store. Between any of these, another thread might intervene!

Solution: Synchronization

// SAFE: Using synchronized keyword
class SafeCounter {
    private int count = 0;
    
    public synchronized void increment() {
        count++; // Now atomic as a whole method
    }
    
    public synchronized int getCount() {
        return count;
    }
}

// Alternative: Synchronized block (more granular)
class SafeCounterBlock {
    private int count = 0;
    private final Object lock = new Object();
    
    public void increment() {
        synchronized(lock) {
            count++;
        }
    }
}

Synchronization guarantees:

  1. Mutual exclusion - Only one thread executes synchronized code at a time
  2. Visibility - Changes made by one thread are visible to others
ApproachScopeLock ObjectWhen to Use
synchronized methodEntire methodthis (instance)Simple, whole-method protection
synchronized(this)Code blockthis (instance)Part of method needs sync
synchronized(object)Code blockSpecific objectMultiple independent locks
static synchronizedStatic methodClass objectProtecting static fields

3. The Java Memory Model (JMM) ๐Ÿงฎ

The JMM defines how threads interact through memory. Without understanding it, your concurrent code may work on your machine but fail mysteriously elsewhere.

Key concepts:

Visibility Problem:

// Thread 1
public void writer() {
    sharedFlag = true; // Might stay in CPU cache!
}

// Thread 2
public void reader() {
    while (!sharedFlag) {
        // Might loop forever!
    }
}

Solution: volatile keyword

private volatile boolean sharedFlag = false;

// Now:
// - Writes are immediately visible to all threads
// - Prevents compiler/CPU reordering
// - BUT: volatile doesn't guarantee atomicity for compound operations!
KeywordAtomicityVisibilityUse Case
volatileโŒ (only for reads/writes)โœ…Flags, status indicators
synchronizedโœ… (for entire block)โœ…Compound operations, consistency
AtomicIntegerโœ…โœ…Counters, lock-free algorithms

๐Ÿง  Memory Device: "VASCO" for volatile usage:

  • Visibility guaranteed
  • Atomic reads/writes only
  • Single variable operations
  • Cheap (no locking overhead)
  • Ordering preserved (happens-before)

4. Wait/Notify Pattern ๐Ÿ“ข

For threads to coordinate, Java provides wait(), notify(), and notifyAll().

class ProducerConsumer {
    private final Queue<Integer> queue = new LinkedList<>();
    private final int MAX_SIZE = 10;
    private final Object lock = new Object();
    
    public void produce(int item) throws InterruptedException {
        synchronized(lock) {
            while (queue.size() == MAX_SIZE) {
                lock.wait(); // Release lock and wait
            }
            queue.add(item);
            lock.notifyAll(); // Wake up consumers
        }
    }
    
    public int consume() throws InterruptedException {
        synchronized(lock) {
            while (queue.isEmpty()) {
                lock.wait(); // Release lock and wait
            }
            int item = queue.remove();
            lock.notifyAll(); // Wake up producers
            return item;
        }
    }
}

โš ๏ธ Always use while loops with wait(), never if! Spurious wakeups can occurโ€”threads may wake up without being notified.

WAIT/NOTIFY FLOW

  Producer Thread              Consumer Thread
       โ”‚                            โ”‚
       โ”‚ synchronized(lock)         โ”‚
       โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”            โ”‚
       โ”‚ queue full?  โ”‚            โ”‚
       โ”‚ YES โ†’ wait() โ”‚            โ”‚
       โ”‚ (releases    โ”‚            โ”‚
       โ”‚  lock) zzz   โ”‚            โ”‚
       โ”‚      โ”‚       โ”‚            โ”‚ synchronized(lock)
       โ”‚      โ”‚       โ”‚            โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
       โ”‚      โ”‚       โ”‚            โ”‚ consume item โ”‚
       โ”‚      โ”‚       โ”‚            โ”‚ notifyAll()  โ”‚
       โ”‚      โ”‚       โ”‚            โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
       โ”‚      โ”‚ โ—„โ”€โ”€โ”€โ”€โ”€โ”€notifiesโ”€โ”€โ”€โ”€โ”‚
       โ”‚ wakes up     โ”‚            โ”‚
       โ”‚ re-acquires  โ”‚            โ”‚
       โ”‚ lock         โ”‚            โ”‚
       โ”‚ adds item    โ”‚            โ”‚
       โ”‚ notifyAll()  โ”‚            โ”‚
       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜            โ”‚

5. Thread Pools & Executors ๐ŸŠ

Creating threads is expensive. Thread pools reuse threads for multiple tasks.

import java.util.concurrent.*;

// Create a fixed-size thread pool
ExecutorService executor = Executors.newFixedThreadPool(4);

// Submit tasks
for (int i = 0; i < 10; i++) {
    final int taskId = i;
    executor.submit(() -> {
        System.out.println("Task " + taskId + " on " + 
                          Thread.currentThread().getName());
    });
}

// Shutdown when done
executor.shutdown(); // No new tasks accepted
try {
    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
        executor.shutdownNow(); // Force shutdown
    }
} catch (InterruptedException e) {
    executor.shutdownNow();
}

Common Executor types:

TypeDescriptionUse Case
newFixedThreadPool(n)Fixed number of threadsBounded resource usage
newCachedThreadPool()Creates threads as needed, reuses idle onesMany short-lived tasks
newSingleThreadExecutor()Single thread, tasks execute sequentiallySequential processing
newScheduledThreadPool(n)Schedule tasks with delays/periodsPeriodic tasks, timers
newWorkStealingPool()Fork/Join based, uses available processorsParallel algorithms

6. Concurrent Collections ๐Ÿ“ฆ

Java provides thread-safe collections that don't require external synchronization.

// Traditional (requires external sync)
Map<String, Integer> map = new HashMap<>();
synchronized(map) {
    map.put("key", 1);
}

// Concurrent (thread-safe built-in)
ConcurrentMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("key", 1); // No external sync needed!

// Other useful concurrent collections
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
ConcurrentSkipListSet<Integer> sortedSet = new ConcurrentSkipListSet<>();

Blocking queues are especially powerful for producer-consumer patterns:

BlockingQueue<Task> queue = new ArrayBlockingQueue<>(100);

// Producer
queue.put(task); // Blocks if queue is full

// Consumer
Task task = queue.take(); // Blocks if queue is empty
CollectionOrderingBest For
ConcurrentHashMapNoneFast concurrent map operations
ConcurrentSkipListMapSortedSorted concurrent map
CopyOnWriteArrayListInsertionMostly reads, few writes
LinkedBlockingQueueFIFOProducer-consumer, unbounded/bounded
PriorityBlockingQueuePriorityTask scheduling by priority

7. Atomic Variables โš›๏ธ

For simple operations, atomic classes provide lock-free thread safety:

import java.util.concurrent.atomic.*;

AtomicInteger counter = new AtomicInteger(0);

// Thread-safe operations without locks
counter.incrementAndGet(); // Returns new value
counter.getAndIncrement(); // Returns old value
counter.addAndGet(5);
counter.compareAndSet(10, 20); // CAS operation

// Custom atomic operation
counter.updateAndGet(current -> current * 2);

Available atomic classes:

  • AtomicInteger, AtomicLong, AtomicBoolean
  • AtomicReference<T> for object references
  • AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray<T>

๐Ÿ’ก Performance tip: Atomic operations use CPU-level instructions (CAS - Compare-And-Swap) that are much faster than locks for simple operations.


Detailed Examples ๐Ÿ”

Example 1: Bank Account Transfer (Deadlock Prevention)

class BankAccount {
    private double balance;
    private final int id;
    
    public BankAccount(int id, double initialBalance) {
        this.id = id;
        this.balance = initialBalance;
    }
    
    public int getId() { return id; }
    
    // WRONG: Can cause deadlock!
    public void transferWrong(BankAccount to, double amount) {
        synchronized(this) {
            synchronized(to) {
                // If Thread A: account1.transfer(account2, 100)
                // And Thread B: account2.transfer(account1, 50)
                // DEADLOCK! Each holds one lock, waits for the other
                this.balance -= amount;
                to.balance += amount;
            }
        }
    }
    
    // CORRECT: Lock ordering prevents deadlock
    public void transfer(BankAccount to, double amount) {
        BankAccount first = this.id < to.id ? this : to;
        BankAccount second = this.id < to.id ? to : this;
        
        synchronized(first) {
            synchronized(second) {
                // Always acquire locks in same order (by ID)
                // Prevents circular wait condition
                if (this.balance >= amount) {
                    this.balance -= amount;
                    to.balance += amount;
                    System.out.println("Transferred " + amount);
                }
            }
        }
    }
}

๐ŸŒ Real-world analogy: Imagine two people trying to exchange items, but each refuses to give theirs until receiving the other's. They're stuck forever! Lock ordering is like establishing a rule: "Lower ID always goes first."

Example 2: Thread-Safe Singleton (Double-Checked Locking)

class DatabaseConnection {
    // volatile ensures visibility of the instance across threads
    private static volatile DatabaseConnection instance;
    private Connection connection;
    
    private DatabaseConnection() {
        // Expensive initialization
        this.connection = createConnection();
    }
    
    public static DatabaseConnection getInstance() {
        // First check (no locking) - performance optimization
        if (instance == null) {
            // Only lock if instance is null
            synchronized(DatabaseConnection.class) {
                // Second check (with lock) - safety
                if (instance == null) {
                    instance = new DatabaseConnection();
                }
            }
        }
        return instance;
    }
    
    private Connection createConnection() {
        // Simulate expensive operation
        return null; // Placeholder
    }
}

Why double-checked locking?

  1. Without first check: Every call synchronizes (slow)
  2. Without second check: Two threads might both see null and create two instances
  3. Without volatile: Partial construction visible to other threads

Example 3: Parallel Sum with Fork/Join

import java.util.concurrent.*;

class ParallelSum extends RecursiveTask<Long> {
    private final long[] array;
    private final int start;
    private final int end;
    private static final int THRESHOLD = 10000;
    
    public ParallelSum(long[] array, int start, int end) {
        this.array = array;
        this.start = start;
        this.end = end;
    }
    
    @Override
    protected Long compute() {
        int length = end - start;
        
        // Small enough? Compute directly
        if (length <= THRESHOLD) {
            long sum = 0;
            for (int i = start; i < end; i++) {
                sum += array[i];
            }
            return sum;
        }
        
        // Split into two subtasks
        int mid = start + length / 2;
        ParallelSum leftTask = new ParallelSum(array, start, mid);
        ParallelSum rightTask = new ParallelSum(array, mid, end);
        
        // Fork left task (async)
        leftTask.fork();
        // Compute right task (sync)
        long rightResult = rightTask.compute();
        // Join left result
        long leftResult = leftTask.join();
        
        return leftResult + rightResult;
    }
    
    public static void main(String[] args) {
        long[] numbers = new long[1_000_000];
        for (int i = 0; i < numbers.length; i++) {
            numbers[i] = i + 1;
        }
        
        ForkJoinPool pool = new ForkJoinPool();
        ParallelSum task = new ParallelSum(numbers, 0, numbers.length);
        long result = pool.invoke(task);
        
        System.out.println("Sum: " + result);
    }
}
FORK/JOIN DIVIDE-AND-CONQUER

        [0...1M]
           โ”‚
      โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”
      โ”‚         โ”‚
   [0..500K] [500K..1M]
      โ”‚         โ”‚
   โ”Œโ”€โ”€โ”ดโ”€โ”€โ”   โ”Œโ”€โ”€โ”ดโ”€โ”€โ”
   โ”‚     โ”‚   โ”‚     โ”‚
 [0..   โ”‚   โ”‚    โ”‚
 250K]  โ”‚   โ”‚    โ”‚
  โ”‚    โ”‚   โ”‚   โ”‚
 fork  โ”‚  compute โ”‚
  โ†“    โ†“   โ†“   โ†“
 sum1 sum2 sum3 sum4
  โ”‚    โ”‚    โ”‚    โ”‚
  โ””โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜
        merge
         โ”‚
         โ–ผ
    Final Sum

Example 4: CompletableFuture for Async Operations

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

class AsyncExample {
    public static void main(String[] args) {
        // Start async computation
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            // Simulate long-running task
            sleep(2000);
            return "Result from async task";
        });
        
        // Chain operations
        CompletableFuture<String> processed = future
            .thenApply(result -> result.toUpperCase())
            .thenApply(result -> result + "!!!");
        
        // Combine two futures
        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 42);
        
        CompletableFuture<String> combined = processed.thenCombine(future2, 
            (str, num) -> str + " Number: " + num);
        
        // Handle errors
        combined.exceptionally(ex -> {
            System.err.println("Error: " + ex.getMessage());
            return "Default value";
        });
        
        // Block and get result
        try {
            String result = combined.get(5, TimeUnit.SECONDS);
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

๐Ÿ’ก Modern async pattern: CompletableFuture is Java's answer to JavaScript Promises. Chain operations, combine results, handle errorsโ€”all without callback hell!


Common Mistakes โš ๏ธ

1. Calling run() Instead of start()

โŒ Wrong:

Thread t = new Thread(() -> System.out.println("Hello"));
t.run(); // Executes in CURRENT thread, not a new one!

โœ… Right:

Thread t = new Thread(() -> System.out.println("Hello"));
t.start(); // Creates and starts new thread

2. Forgetting to Shutdown ExecutorService

โŒ Wrong:

ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> doWork());
// Program never terminates! Threads keep JVM alive

โœ… Right:

ExecutorService executor = Executors.newFixedThreadPool(4);
try {
    executor.submit(() -> doWork());
} finally {
    executor.shutdown();
}

3. Synchronizing on null or Mutable Objects

โŒ Wrong:

class Bad {
    private String lock = "initial"; // String literals are interned!
    
    public void method1() {
        synchronized(lock) { // Lock might be shared with other classes!
            lock = "changed"; // Now synchronizing on different object!
        }
    }
}

โœ… Right:

class Good {
    private final Object lock = new Object(); // Dedicated, immutable lock
    
    public void method1() {
        synchronized(lock) {
            // Safe
        }
    }
}

4. Using volatile for Compound Operations

โŒ Wrong:

class Counter {
    private volatile int count = 0;
    
    public void increment() {
        count++; // NOT atomic! Read + modify + write
    }
}

โœ… Right:

class Counter {
    private AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        count.incrementAndGet(); // Atomic operation
    }
}

5. Catching InterruptedException and Ignoring It

โŒ Wrong:

try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    // Swallowing the exception loses interrupt status!
}

โœ… Right:

try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt(); // Restore interrupt status
    // Handle appropriately (log, throw, cleanup)
}

6. Holding Locks While Doing I/O

โŒ Wrong:

public synchronized void saveData(Data data) {
    // Lock held for entire duration of disk I/O!
    writeToDisk(data); // Slow operation
    // Other threads blocked unnecessarily
}

โœ… Right:

public void saveData(Data data) {
    Data copy;
    synchronized(this) {
        copy = data.clone(); // Quick operation under lock
    }
    writeToDisk(copy); // Slow I/O outside lock
}

Key Takeaways ๐ŸŽฏ

  1. Thread Creation: Use Runnable over extending Thread; call start(), not run()

  2. Synchronization: Use synchronized for mutual exclusion and visibility; always lock in consistent order to prevent deadlock

  3. Volatile: Good for flags and status, but doesn't guarantee atomicity for compound operations

  4. Thread Pools: Use ExecutorService instead of creating threads manually; always shut down pools

  5. Concurrent Collections: Use ConcurrentHashMap, BlockingQueue, etc. instead of synchronized wrappers

  6. Atomic Classes: Perfect for counters and simple operations without locking overhead

  7. Wait/Notify: Always use while loops, not if; prefer BlockingQueue for producer-consumer

  8. Modern APIs: Use CompletableFuture for async operations, Fork/Join for parallel algorithms

  9. Error Handling: Always restore interrupt status when catching InterruptedException

  10. Testing: Concurrency bugs are timing-dependent; test with stress tests, race detectors, and varying thread counts


๐Ÿ“š Further Study

  1. Java Concurrency in Practice by Brian Goetz - https://jcip.net/ (The definitive guide to Java concurrency)

  2. Oracle Java Concurrency Tutorial - https://docs.oracle.com/javase/tutorial/essential/concurrency/ (Official documentation with examples)

  3. Baeldung Java Concurrency - https://www.baeldung.com/java-concurrency (Practical tutorials and modern patterns)


๐Ÿ“‹ Quick Reference Card

ConceptKey PointsCode Pattern
Thread CreationImplement Runnable, call start()new Thread(() -> {...}).start();
SynchronizationMutual exclusion + visibilitysynchronized(lock) {...}
VolatileVisibility only, no atomicityprivate volatile boolean flag;
Wait/NotifyUse while loops, hold lockwhile(condition) lock.wait();
Thread PoolReuse threads, always shutdownExecutorService e = Executors.newFixedThreadPool(4);
AtomicLock-free thread-safe operationsAtomicInteger.incrementAndGet()
Concurrent CollectionsBuilt-in thread safetyConcurrentHashMap, BlockingQueue
CompletableFutureAsync operations, chainableCompletableFuture.supplyAsync(() -> {...})
Deadlock PreventionLock ordering, timeoutsLock in consistent order by ID
Interrupt HandlingRestore interrupt statusThread.currentThread().interrupt();

๐Ÿง  Memory Device - "SWIFT" for thread safety:

  • Synchronize shared mutable data
  • Wait/notify with while loops
  • Immutable objects are automatically thread-safe
  • Final fields guarantee safe publication
  • Thread-local for thread-specific data