Lesson 26 • Advanced

    Multithreading: Threads, Runnables & Executors

    Create and manage threads, understand synchronization, and use thread pools.

    Before You Start

    You should understand OOP & Interfaces (Lessons 9–11), Lambda Expressions (Lesson 22), and Collections (Lesson 13). Multithreading builds on Runnable interfaces and uses lambdas extensively for task definition.

    What You'll Learn

    • ✅ Creating threads: Thread class vs Runnable interface
    • ✅ Thread lifecycle and states
    • ✅ Synchronization and race conditions
    • ✅ ExecutorService and thread pools
    • ✅ Thread-safe collections

    1️⃣ Understanding Concurrency

    💡 Analogy: Restaurant Kitchen

    A single-threaded program is like a restaurant with one chef — every order waits in line. Multithreading is like having multiple chefs (threads) working simultaneously. But they share the same kitchen (memory), so they need coordination: who uses the stove (synchronization), who gets the knife next (locking), and how to avoid two chefs grabbing the same ingredient (race conditions).

    Thread vs Runnable

    Always prefer Runnable (or Callable for return values). Java only allows single inheritance, so extending Thread wastes your inheritance slot. Runnable also separates the task from the execution mechanism, making it work with ExecutorService.

    Try It: Thread Creation & Lifecycle

    Try it Yourself »
    JavaScript
    // Thread Creation & Lifecycle
    console.log("=== Thread Creation ===\n");
    
    // 1. Simulated thread creation
    console.log("1. CREATING THREADS:");
    class SimThread {
        constructor(name, task) { this.name = name; this.task = task; this.state = "NEW"; }
        start() {
            this.state = "RUNNABLE";
            console.log("  [" + this.name + "] State: " + this.state);
            this.state = "RUNNING";
            console.log("  [" + this.name + "] State: " + this.state);
            this.task();
            this.stat
    ...

    2️⃣ Race Conditions & Synchronization

    A race condition occurs when two threads read-modify-write shared data simultaneously. For example, two threads incrementing a counter can both read 5, increment to 6, and write 6 — losing one increment. The synchronized keyword creates a mutex lock that only allows one thread at a time into a critical section.

    Try It: Race Conditions & Synchronization

    Try it Yourself »
    JavaScript
    // Race Conditions & Synchronization
    console.log("=== Race Conditions ===\n");
    
    // 1. Simulated race condition
    console.log("1. RACE CONDITION (simulated):");
    let counter = 0;
    
    // In real multi-threading, these would run simultaneously
    function unsafeIncrement() { 
        let temp = counter;    // Thread A reads 5
        // Thread B also reads 5 here!
        counter = temp + 1;    // Both write 6 — one increment lost!
    }
    
    for (let i = 0; i < 1000; i++) unsafeIncrement();
    console.log("  Counter after 1000 i
    ...

    3️⃣ ExecutorService & Thread Pools

    Creating new Thread() for every task is expensive (~1MB stack per thread) and uncontrolled. ExecutorService manages a pool of reusable threads, queues excess tasks, and provides lifecycle management. In production, you should almost never create threads directly.

    Try It: ExecutorService & Producer-Consumer

    Try it Yourself »
    JavaScript
    // ExecutorService & Producer-Consumer
    console.log("=== ExecutorService ===\n");
    
    // 1. Thread pool simulation
    console.log("1. THREAD POOL (FixedThreadPool):");
    class ThreadPool {
        constructor(size) { this.size = size; this.tasks = []; this.running = 0; }
        submit(name, task) { this.tasks.push({ name, task }); }
        execute() {
            console.log("  Pool size: " + this.size + " threads");
            let threadId = 0;
            this.tasks.forEach(({ name, task }) => {
                let thread = "T
    ...

    Common Mistakes

    Not shutting down ExecutorService: Forgetting executor.shutdown() keeps threads alive and your app won't exit.
    Synchronizing on the wrong object: synchronized(new Object()) creates a new lock each time — useless!
    Calling run() instead of start(): thread.run() executes on the current thread. thread.start() creates a new thread.
    Using Thread.sleep() for coordination: Use CountDownLatch, CyclicBarrier, or CompletableFuture instead.

    Pro Tips

    💡 Thread pool sizing: CPU-bound → availableProcessors() threads. I/O-bound → 2x-4x that number.

    💡 Prefer AtomicInteger over synchronized for simple counters — lock-free via hardware CAS.

    💡 Virtual threads (Java 21+) are lightweight (~KB) and you can create millions — game-changing for I/O.

    📋 Quick Reference

    ConceptJava APIUse Case
    Thread poolExecutors.newFixedThreadPool(n)Reuse threads
    Synchronizesynchronized keywordPrevent race conditions
    AtomicAtomicInteger, AtomicLongLock-free thread safety
    ConcurrentConcurrentHashMapThread-safe collections

    🎉 Lesson Complete!

    You understand Java multithreading fundamentals!

    Next: Concurrency Utilities — Locks, Semaphores & Latches.

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service