Multithreading

Threads, synchronization, and concurrency

1. Introduction to Multithreading

Single-Threaded Execution

  • Tasks execute one after another
  • CPU cores underutilized
  • Blocking operations halt entire program
  • Predictable but slow performance
  • Simple to debug and understand

Multi-Threaded Execution

  • Tasks execute concurrently
  • Better CPU utilization across cores
  • Non-blocking operations possible
  • Complex but efficient performance
  • Requires careful synchronization

Sequential vs Concurrent Execution

// Sequential execution example
public class SequentialExample {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        // Task 1 - takes 3 seconds
        performTask("Database Operation", 3000);

        // Task 2 - takes 2 seconds
        performTask("File Processing", 2000);

        // Task 3 - takes 1 second
        performTask("Network Call", 1000);

        long end = System.currentTimeMillis();
        System.out.println("Total time: " + (end - start) + "ms");
        // Output: Total time: ~6000ms
    }

    static void performTask(String taskName, int duration) {
        System.out.println("Starting " + taskName);
        try {
            Thread.sleep(duration);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("Completed " + taskName);
    }
}

// Concurrent execution example
public class ConcurrentExample {
    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();

        // Create threads for each task
        Thread task1 = new Thread(() -> performTask("Database Operation", 3000));
        Thread task2 = new Thread(() -> performTask("File Processing", 2000));
        Thread task3 = new Thread(() -> performTask("Network Call", 1000));

        // Start all threads simultaneously
        task1.start();
        task2.start();
        task3.start();

        // Wait for all to complete
        task1.join();
        task2.join();
        task3.join();

        long end = System.currentTimeMillis();
        System.out.println("Total time: " + (end - start) + "ms");
        // Output: Total time: ~3000ms (limited by slowest task)
    }
}

Benefits of Multithreading

  • Improved application responsiveness
  • Better resource utilization
  • Concurrent task execution
  • Enhanced user experience
  • Scalability on multi-core systems

Common Use Cases

  • GUI applications (UI + background tasks)
  • Web servers (handling multiple requests)
  • Data processing pipelines
  • Producer-consumer scenarios
  • Parallel algorithms

Challenges to Consider

  • Race conditions and data corruption
  • Deadlocks and livelocks
  • Resource contention
  • Debugging complexity
  • Memory consistency issues

2. Threading Basics

Process vs Thread

  • Process: Independent program with own memory space
  • Thread: Lightweight process sharing memory within process
  • Threads share heap memory but have separate stacks
  • Context switching between threads is faster than processes

Thread Components

  • Program Counter: Points to current instruction
  • Stack: Local variables and method calls
  • Registers: CPU register values
  • State: Current execution state
  • Priority: Scheduling priority level

Thread Types in Java

  • User Threads: Normal application threads
  • Daemon Threads: Background service threads
  • Main Thread: Entry point of application
  • Worker Threads: Execute specific tasks

Thread Information and Properties

public class ThreadBasicsDemo {
    public static void main(String[] args) {
        // Get information about current thread
        Thread currentThread = Thread.currentThread();

        System.out.println("=== Current Thread Information ===");
        System.out.println("Name: " + currentThread.getName());
        System.out.println("ID: " + currentThread.getId());
        System.out.println("Priority: " + currentThread.getPriority());
        System.out.println("State: " + currentThread.getState());
        System.out.println("Is Alive: " + currentThread.isAlive());
        System.out.println("Is Daemon: " + currentThread.isDaemon());

        // System information
        System.out.println("\n=== System Information ===");
        System.out.println("Available Processors: " +
                         Runtime.getRuntime().availableProcessors());
        System.out.println("Active Thread Count: " +
                         Thread.activeCount());

        // Create and examine a custom thread
        Thread customThread = new Thread(() -> {
            System.out.println("\n=== Custom Thread Executing ===");
            System.out.println("Custom thread name: " + Thread.currentThread().getName());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Custom thread work completed");
        }, "MyCustomThread");

        // Set thread properties
        customThread.setPriority(Thread.NORM_PRIORITY + 1);
        customThread.setDaemon(false);

        System.out.println("\n=== Before Start ===");
        System.out.println("State: " + customThread.getState());

        customThread.start();

        System.out.println("\n=== After Start ===");
        System.out.println("State: " + customThread.getState());

        try {
            customThread.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("\n=== After Completion ===");
        System.out.println("State: " + customThread.getState());
    }
}

3. Thread Creation Methods

Method 1: Extending Thread Class

class CustomThread extends Thread {
    private String taskName;
    private int iterations;

    public CustomThread(String taskName, int iterations) {
        super(taskName);
        this.taskName = taskName;
        this.iterations = iterations;
    }

    @Override
    public void run() {
        System.out.println(taskName + " started by " + getName());

        for (int i = 1; i " - Iteration " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }
        }
        System.out.println(taskName + " completed");
    }
}

// Usage
CustomThread thread1 = new CustomThread("DataProcessor", 3);
thread1.start();

Method 2: Implementing Runnable Interface (Recommended)

class TaskRunner implements Runnable {
    private String taskName;
    private int delay;

    public TaskRunner(String taskName, int delay) {
        this.taskName = taskName;
        this.delay = delay;
    }

    @Override
    public void run() {
        System.out.println(taskName + " started in thread: " +
                         Thread.currentThread().getName());
        try {
            Thread.sleep(delay);
            if (Thread.currentThread().isInterrupted()) {
                throw new InterruptedException("Task was interrupted");
            }
        } catch (InterruptedException e) {
            System.out.println(taskName + " was interrupted");
            Thread.currentThread().interrupt();
        }
        System.out.println(taskName + " finished");
    }
}

// Usage
TaskRunner task = new TaskRunner("EmailSender", 800);
Thread thread = new Thread(task, "Email-Thread");
thread.start();

Method 3: Using ExecutorService (Production Recommended)

import java.util.concurrent.*;
import java.util.List;
import java.util.ArrayList;

public class ExecutorServiceExample {
    public static void main(String[] args) {
        // Create different types of thread pools
        ExecutorService fixedPool = Executors.newFixedThreadPool(3);
        ExecutorService cachedPool = Executors.newCachedThreadPool();
        ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2);

        try {
            // Submit Runnable tasks
            for (int i = 1; i 5; i++) {
                final int taskId = i;
                fixedPool.submit(() -> {
                    System.out.println("Task " + taskId + " running in: " +
                                     Thread.currentThread().getName());
                    try { Thread.sleep(2000); }
                    catch (InterruptedException e) { Thread.currentThread().interrupt(); }
                    System.out.println("Task " + taskId + " completed");
                });
            }

            // Submit Callable tasks with return values
            List<Future<String>> futures = new ArrayList<>();
            for (int i = 1; i 4; i++) {
                final int taskId = i;
                Future<String> future = cachedPool.submit(() -> {
                    Thread.sleep(1000 + taskId * 300);
                    return "Result from task " + taskId;
                });
                futures.add(future);
            }

            // Collect results with timeout
            for (int i = 0; i try {
                    String result = futures.get(i).get(3, TimeUnit.SECONDS);
                    System.out.println(result);
                } catch (TimeoutException e) {
                    System.out.println("Task timed out");
                    futures.get(i).cancel(true);
                } catch (ExecutionException e) {
                    System.out.println("Task failed: " + e.getCause());
                }
            }

            // Scheduled tasks
            scheduledPool.scheduleWithFixedDelay(() -> {
                System.out.println("Periodic task at: " + System.currentTimeMillis());
            }, 0, 1, TimeUnit.SECONDS);

            Thread.sleep(5000);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            // Proper shutdown
            fixedPool.shutdown();
            cachedPool.shutdown();
            scheduledPool.shutdown();
        }
    }
}
MethodProsConsBest Use Case
Extend ThreadSimple, direct controlNo multiple inheritanceSimple standalone threads
Implement RunnableBetter OOP design, flexibleMore verboseWhen inheritance flexibility needed
ExecutorServiceThread pool managementMore complex APIProduction applications
CompletableFutureModern, composableLearning curveAsync pipelines

4. Thread Lifecycle and States

NEW

Thread created but not started

Created with new Thread() but start() not called

RUNNABLE

Thread executing or ready to execute

Either running or waiting for CPU time

BLOCKED

Thread blocked waiting for monitor lock

Waiting to enter synchronized block

WAITING

Thread waiting indefinitely

Called wait(), join(), or LockSupport.park()

TIMED_WAITING

Thread waiting for specified time

Called sleep(), wait(timeout), or join(timeout)

TERMINATED

Thread completed execution

run() method finished or exception occurred

Thread Lifecycle Demonstration

public class ThreadLifecycleDemo {
    public static void main(String[] args) throws InterruptedException {
        Object sharedLock = new Object();

        Thread workerThread = new Thread(() -> {
            try {
                System.out.println("Worker: Starting execution");

                // Demonstrate TIMED_WAITING state
                System.out.println("Worker: Going to sleep");
                Thread.sleep(2000);
                System.out.println("Worker: Woke up");

                // Demonstrate WAITING state
                synchronized (sharedLock) {
                    System.out.println("Worker: Waiting for notification");
                    sharedLock.wait();
                    System.out.println("Worker: Received notification");
                }

                System.out.println("Worker: Completing execution");

            } catch (InterruptedException e) {
                System.out.println("Worker: Was interrupted");
                Thread.currentThread().interrupt();
            }
        }, "WorkerThread");

        // NEW state
        System.out.println("Initial state: " + workerThread.getState());

        workerThread.start();

        // Let worker enter sleep state
        Thread.sleep(1000);
        System.out.println("During sleep: " + workerThread.getState());

        // Let worker enter wait state
        Thread.sleep(2000);
        System.out.println("During wait: " + workerThread.getState());

        // Notify worker
        Thread.sleep(1000);
        synchronized (sharedLock) {
            System.out.println("Main: Notifying worker");
            sharedLock.notify();
        }

        workerThread.join();
        System.out.println("Final state: " + workerThread.getState());
    }
}

Best Practice

Always use cooperative interruption rather than deprecated methods like stop(). Check Thread.currentThread().isInterrupted() regularly in long-running loops and handle InterruptedException properly.

5. Synchronization Mechanisms

The Race Condition Problem

class UnsafeCounter {
    private int count = 0;

    public void increment() {
        // This operation is NOT atomic!
        // 1. Read current value of count
        // 2. Add 1 to the value
        // 3. Write the result back to count
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class RaceConditionDemo {
    public static void main(String[] args) throws InterruptedException {
        UnsafeCounter counter = new UnsafeCounter();
        int numThreads = 10;
        int incrementsPerThread = 1000;

        Thread[] threads = new Thread[numThreads];

        for (int i = 0; i new Thread(() -> {
                for (int j = 0; j for (Thread thread : threads) thread.start();
        for (Thread thread : threads) thread.join();

        int expected = numThreads * incrementsPerThread;
        System.out.println("Expected: " + expected);
        System.out.println("Actual: " + counter.getCount());
        System.out.println("Lost updates: " + (expected - counter.getCount()));
    }
}

Synchronized Solution

class SafeCounter {
    private int count = 0;
    private final Object lock = new Object();

    // Synchronized method - uses intrinsic lock
    public synchronized void increment() {
        count++;
    }

    // Synchronized block with custom lock object
    public void decrement() {
        synchronized (lock) {
            count--;
        }
    }

    public synchronized int getCount() {
        return count;
    }
}

// Thread-safe bank account example
class BankAccount {
    private double balance;
    private final Object balanceLock = new Object();

    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    }

    public void deposit(double amount) {
        if (amount 0) throw new IllegalArgumentException("Amount must be positive");

        synchronized (balanceLock) {
            double oldBalance = balance;
            balance += amount;
            System.out.println(Thread.currentThread().getName() +
                             " deposited $" + amount +
                             " (Balance: $" + oldBalance + " -> $" + balance + ")");
        }
    }

    public boolean withdraw(double amount) {
        synchronized (balanceLock) {
            if (balance >= amount) {
                balance -= amount;
                System.out.println(Thread.currentThread().getName() +
                                 " withdrew $" + amount);
                return true;
            }
            return false;
        }
    }

    public synchronized double getBalance() {
        return balance;
    }
}

6. Concurrent Collections

ConcurrentHashMap Example

import java.util.concurrent.*;

public class ConcurrentHashMapDemo {
    public static void main(String[] args) throws InterruptedException {
        ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
        concurrentMap.put("globalCounter", 0);

        Thread[] threads = new Thread[5];

        for (int i = 0; i 5; i++) {
            final int threadId = i;
            threads[i] = new Thread(() -> {
                for (int j = 0; j 1000; j++) {
                    // Atomic increment using compute
                    concurrentMap.compute("globalCounter", (key, val) ->
                        val == null ? 1 : val + 1);

                    // Atomic addition using merge
                    concurrentMap.merge("thread-" + threadId, 1, Integer::sum);
                }
            });
        }

        for (Thread thread : threads) thread.start();
        for (Thread thread : threads) thread.join();

        System.out.println("Global counter: " + concurrentMap.get("globalCounter"));
        System.out.println("Expected: 5000");

        // Safe iteration
        concurrentMap.forEach((key, value) -> System.out.println(key + " = " + value));
    }
}

BlockingQueue - Producer-Consumer Pattern

import java.util.concurrent.*;

public class BlockingQueueDemo {
    private static final String POISON_PILL = "STOP";

    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);

        // Producer
        Thread producer = new Thread(() -> {
            try {
                for (int i = 1; i 20; i++) {
                    String item = "Task-" + i;
                    System.out.println("[Producer] Creating: " + item);
                    queue.put(item); // Blocks if queue is full
                    Thread.sleep(150);
                }
                queue.put(POISON_PILL);
                queue.put(POISON_PILL);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "Producer");

        // Consumers
        Thread consumer1 = new Thread(() -> consume(queue), "Consumer-1");
        Thread consumer2 = new Thread(() -> consume(queue), "Consumer-2");

        producer.start();
        consumer1.start();
        consumer2.start();

        producer.join();
        consumer1.join();
        consumer2.join();

        System.out.println("Demo completed");
    }

    private static void consume(BlockingQueue<String> queue) {
        try {
            while (true) {
                String item = queue.take(); // Blocks if queue is empty
                if (item.equals(POISON_PILL)) {
                    System.out.println("[" + Thread.currentThread().getName() + "] Stopping");
                    break;
                }
                System.out.println("[" + Thread.currentThread().getName() + "] Processing: " + item);
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
CollectionUse CaseRead PerformanceWrite Performance
ConcurrentHashMapHigh concurrency read/writeExcellentExcellent
CopyOnWriteArrayListRead-heavy, rare writesExcellentPoor
ArrayBlockingQueueBounded producer-consumerGoodGood
LinkedBlockingQueueUnbounded producer-consumerGoodGood

7. Advanced Concurrency Utilities

CountDownLatch - Service Startup Coordination

import java.util.concurrent.*;

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        int numberOfServices = 4;
        CountDownLatch allServicesReady = new CountDownLatch(numberOfServices);

        String[] services = {"Database", "Cache", "Web", "Security"};
        int[] startupTimes = {2000, 1500, 1000, 2500};

        System.out.println("=== Application Startup ===");
        long start = System.currentTimeMillis();

        for (int i = 0; i final int index = i;
            new Thread(() -> {
                try {
                    System.out.println(services[index] + " starting...");
                    Thread.sleep(startupTimes[index]);
                    System.out.println(services[index] + " started");
                    allServicesReady.countDown();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }).start();
        }

        System.out.println("Waiting for all services...");
        allServicesReady.await();

        long end = System.currentTimeMillis();
        System.out.println("All services started! Total time: " + (end - start) + "ms");
    }
}

Semaphore - Resource Pool Management

import java.util.concurrent.*;

class ConnectionPool {
    private final Semaphore availableConnections;
    private final int maxConnections;

    public ConnectionPool(int maxConnections) {
        this.maxConnections = maxConnections;
        this.availableConnections = new Semaphore(maxConnections, true);
    }

    public void getConnection() throws InterruptedException {
        availableConnections.acquire();
        System.out.println("[" + Thread.currentThread().getName() +
                         "] Acquired connection (available: " +
                         availableConnections.availablePermits() + ")");
    }

    public void releaseConnection() {
        System.out.println("[" + Thread.currentThread().getName() +
                         "] Released connection");
        availableConnections.release();
    }
}

public class SemaphoreDemo {
    public static void main(String[] args) throws InterruptedException {
        ConnectionPool pool = new ConnectionPool(3);
        int numberOfClients = 8;

        Thread[] clients = new Thread[numberOfClients];

        for (int i = 0; i new Thread(() -> {
                try {
                    pool.getConnection();
                    Thread.sleep(1000 + (int)(Math.random() * 2000));
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    pool.releaseConnection();
                }
            }, "Client-" + i);
        }

        for (Thread client : clients) client.start();
        for (Thread client : clients) client.join();

        System.out.println("All clients completed");
    }
}

Performance Tip

Use fair semaphores (true parameter) when you need to prevent thread starvation, but note that fair semaphores have slightly lower performance than unfair ones.

8. Best Practices and Performance

Thread Safety Principles

  • Prefer immutable objects when possible
  • Minimize shared mutable state
  • Use thread-safe collections
  • Avoid shared variables between threads
  • Use defensive copying for mutable objects

Performance Guidelines

  • Use thread pools instead of creating threads
  • Choose appropriate pool sizes
  • Avoid excessive synchronization
  • Use lock-free algorithms when possible
  • Profile and measure performance

Error Handling

  • Always handle InterruptedException properly
  • Use UncaughtExceptionHandler for critical threads
  • Implement proper shutdown sequences
  • Use timeouts to avoid infinite waits
  • Log thread-related errors with context

Production Thread Pool Configuration

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class ProductionThreadPoolExample {

    // Custom thread factory for better debugging
    private static class NamedThreadFactory implements ThreadFactory {
        private final String namePrefix;
        private final AtomicInteger threadNumber = new AtomicInteger(1);

        public NamedThreadFactory(String namePrefix) {
            this.namePrefix = namePrefix;
        }

        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r, namePrefix + "-" + threadNumber.getAndIncrement());
            thread.setUncaughtExceptionHandler((t, e) -> {
                System.err.println("Thread " + t.getName() + " threw: " + e.getMessage());
            });
            return thread;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // CPU-bound task pool
        int cpuCores = Runtime.getRuntime().availableProcessors();
        ThreadPoolExecutor cpuBoundPool = new ThreadPoolExecutor(
            cpuCores,
            cpuCores,
            60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100),
            new NamedThreadFactory("CPU-Worker")
        );

        // I/O-bound task pool
        ThreadPoolExecutor ioBoundPool = new ThreadPoolExecutor(
            10,
            50,
            60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(200),
            new NamedThreadFactory("IO-Worker"),
            new ThreadPoolExecutor.CallerRunsPolicy()
        );

        try {
            // Submit tasks...
            for (int i = 0; i 10; i++) {
                final int taskId = i;
                cpuBoundPool.submit(() -> {
                    System.out.println("CPU Task " + taskId + " running");
                });
            }

            Thread.sleep(5000);

        } finally {
            shutdownGracefully(cpuBoundPool, "CPU Pool");
            shutdownGracefully(ioBoundPool, "I/O Pool");
        }
    }

    private static void shutdownGracefully(ExecutorService executor, String name) {
        executor.shutdown();
        try {
            if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }
}

9. Interview Questions

Conceptual Questions

  • Q: What is the difference between process and thread?
  • Q: Explain synchronized vs volatile keywords.
  • Q: What is a race condition and how to prevent it?
  • Q: When to use CountDownLatch vs CyclicBarrier?

Coding Questions

  • Q: Implement a thread-safe singleton.
  • Q: Create a producer-consumer solution.
  • Q: Print numbers 1-100 using 3 threads alternately.
  • Q: Implement a thread-safe counter.

Advanced Questions

  • Q: How does ConcurrentHashMap achieve thread safety?
  • Q: Explain happens-before relationship.
  • Q: How to detect and prevent deadlocks?
  • Q: What are lock-free algorithms?

Sample Interview Coding Problem

// Problem: Implement a thread-safe counter with increment,
// decrement, and get operations.

import java.util.concurrent.atomic.AtomicInteger;

// Solution 1: Using AtomicInteger (preferred)
class AtomicCounter {
    private final AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();
    }

    public void decrement() {
        count.decrementAndGet();
    }

    public int get() {
        return count.get();
    }
}

// Test
public class CounterTest {
    public static void main(String[] args) throws InterruptedException {
        AtomicCounter counter = new AtomicCounter();
        int numThreads = 10;
        int ops = 1000;

        Thread[] threads = new Thread[numThreads];

        for (int i = 0; i if (i % 2 == 0) {
                threads[i] = new Thread(() -> {
                    for (int j = 0; j else {
                threads[i] = new Thread(() -> {
                    for (int j = 0; j for (Thread t : threads) t.start();
        for (Thread t : threads) t.join();

        System.out.println("Final value: " + counter.get());
        System.out.println("Expected: 0");
    }
}

Interview Success Tips

  • Understand Fundamentals: Know thread lifecycle, states, and basic synchronization primitives
  • Practice Coding: Implement common patterns like producer-consumer and thread pools
  • Know Collections: Understand when to use ConcurrentHashMap vs CopyOnWriteArrayList
  • Explain Trade-offs: Discuss performance vs complexity and safety vs speed
  • Real-world Examples: Be ready to discuss practical applications