Java Tutorial 0/145 lessons ~6 min read Lesson 29

    Multithreading & Concurrency

    java multithreading concurrency threads Runnable Callable synchronized volatile ReentrantLock ExecutorService CompletableFuture ConcurrentHashMap

    Course progress0%
    Focus
    31 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Modern applications rarely execute only one task at a time.

    Consider a banking application.

    While one user is:

    • Transferring money

    Another user is:

    • Checking account balance

    At the same time, the system is:

    • Sending email notifications
    • Processing transactions
    • Writing audit logs
    • Generating reports
    • Communicating with payment gateways

    If these tasks execute sequentially, the application becomes slow and unresponsive.

    Java solves this problem using Multithreading and Concurrency.

    Multithreading allows multiple threads to execute simultaneously (or concurrently, depending on available CPU cores), improving responsiveness, throughput, and resource utilization.

    Multithreading is heavily used in:

    • Spring Boot Applications
    • Microservices
    • Web Servers (Tomcat, Jetty)
    • Banking Systems
    • E-Commerce Platforms
    • Kafka Consumers
    • REST APIs
    • Android Applications
    • High-Frequency Trading Systems

    It is one of the most important advanced Java topics and is frequently asked in senior-level interviews.

    In this lesson, you'll learn:

    • What is a Process?
    • What is a Thread?
    • Multithreading
    • Thread Lifecycle
    • Creating Threads
    • Runnable Interface
    • Callable Interface
    • Thread Synchronization
    • Race Conditions
    • synchronized Keyword
    • volatile Keyword
    • Locks (ReentrantLock)
    • Atomic Classes
    • Executor Framework
    • Thread Pools
    • CompletableFuture
    • Fork/Join Framework
    • Concurrent Collections
    • Best Practices
    • Common Mistakes
    • Hands-on Exercises
    • Professional Interview Questions

    What is a Process?

    A Process is an independent program running in memory.

    Examples:

    • Google Chrome
    • IntelliJ IDEA
    • Spotify
    • Microsoft Word

    Each process has:

    • Own memory
    • Own resources
    • Own execution environment

    What is a Thread?

    A Thread is the smallest unit of execution inside a process.

    Example:

    code
    Chrome Process
    ├── UI Thread
    ├── Network Thread
    ├── Rendering Thread
    └── Download Thread

    All threads inside a process share:

    • Heap Memory
    • Open Files
    • Network Connections

    But each thread has its own:

    • Stack
    • Program Counter
    • Local Variables

    Process vs Thread

    ProcessThread
    Independent executionExecutes within a process
    Separate memoryShared heap memory
    HeavyweightLightweight
    Slower creationFaster creation
    Communication is expensiveCommunication is easier

    What is Multithreading?

    Multithreading is the ability to execute multiple threads concurrently.

    Example:

    code
    Main Thread
    Thread 1 → Download File
    Thread 2 → Read Database
    Thread 3 → Send Email
    Thread 4 → Generate PDF

    Benefits:

    • Better CPU utilization
    • Faster execution
    • Improved responsiveness
    • Better scalability

    Thread Lifecycle

    code
    New
    Runnable
    Running
    Blocked / Waiting
    Runnable
    Terminated

    States:

    • NEW
    • RUNNABLE
    • BLOCKED
    • WAITING
    • TIMED_WAITING
    • TERMINATED

    Creating a Thread

    Extending the Thread Class

    code
    class MyThread extends Thread {
    @Override
    public void run() {
    System.out.println(
    "Thread Running");
    }
    }
    public class Main {
    public static void main(String[] args) {
    MyThread thread =
    new MyThread();
    thread.start();
    }
    }

    Output:

    code
    Thread Running

    Always call start(), not run(). Calling run() directly executes the method on the current thread.

    Creating Threads using Runnable

    Preferred approach:

    code
    class Task implements Runnable {
    @Override
    public void run() {
    System.out.println(
    "Runnable Executed");
    }
    }
    public class Main {
    public static void main(String[] args) {
    Thread thread =
    new Thread(new Task());
    thread.start();
    }
    }

    Advantages:

    • Better design
    • Supports inheritance from another class
    • Encourages separation of task and thread

    Runnable using Lambda

    code
    Thread thread = new Thread(() ->
    System.out.println(
    "Lambda Thread")
    );
    thread.start();

    This is the most common modern style.

    Callable Interface

    Runnable cannot return values.

    Callable can.

    code
    Callable<Integer> task = () -> {
    return 100;
    };

    Used with:

    • ExecutorService
    • Future

    Future

    code
    ExecutorService executor =
    Executors.newSingleThreadExecutor();
    Future<Integer> future =
    executor.submit(() -> 500);
    System.out.println(
    future.get());
    executor.shutdown();

    Output:

    code
    500

    Race Condition

    Suppose two threads update the same variable.

    code
    count++;

    Thread A:

    code
    Reads 10
    Writes 11

    Thread B:

    code
    Reads 10
    Writes 11

    Expected:

    code
    12

    Actual:

    code
    11

    This is called a Race Condition.

    Synchronization

    Synchronization ensures that only one thread enters a critical section at a time.

    code
    public synchronized void increment(){
    count++;
    }

    This prevents concurrent modification of shared state.

    Synchronized Block

    code
    synchronized(this){
    count++;
    }

    Synchronizing only the critical section reduces contention.

    volatile Keyword

    volatile ensures visibility of updates across threads.

    code
    private volatile boolean running = true;

    Use volatile when:

    • Multiple threads read/write a variable
    • Atomicity is not required
    • Visibility is required

    volatile does not make compound operations such as count++ atomic.

    ReentrantLock

    Provides advanced locking features beyond synchronized.

    code
    Lock lock = new ReentrantLock();
    lock.lock();
    try{
    count++;
    }
    finally{
    lock.unlock();
    }

    Advantages:

    • Try Lock
    • Timed Lock
    • Interruptible Lock
    • Fair Locking

    Atomic Classes

    Package:

    code
    java.util.concurrent.atomic

    Example:

    code
    AtomicInteger counter =
    new AtomicInteger();
    counter.incrementAndGet();
    System.out.println(
    counter.get());

    Atomic classes perform thread-safe operations without explicit synchronization for many common use cases.

    Executor Framework

    Instead of manually creating threads:

    code
    ExecutorService executor =
    Executors.newFixedThreadPool(4);
    executor.submit(() ->
    System.out.println("Task")
    );
    executor.shutdown();

    Benefits:

    • Thread Reuse
    • Better Performance
    • Resource Management
    • Scalable Design

    Thread Pool

    code
    Task Queue
    Thread Pool
    Worker Threads

    Instead of creating a new thread for every request, worker threads are reused.

    This is how most web servers operate.

    CompletableFuture

    Introduced in Java 8 for asynchronous programming.

    code
    CompletableFuture
    .supplyAsync(() -> "Java")
    .thenApply(String::toUpperCase)
    .thenAccept(System.out::println);

    Output:

    code
    JAVA

    Benefits:

    • Asynchronous execution
    • Chaining
    • Exception handling
    • Non-blocking programming

    Fork/Join Framework

    Designed for divide-and-conquer algorithms.

    code
    Large Task
    Split
    Subtasks
    Parallel Processing
    Merge Results

    Used for CPU-intensive workloads.

    Concurrent Collections

    Package:

    code
    java.util.concurrent

    Common classes:

    • ConcurrentHashMap
    • CopyOnWriteArrayList
    • BlockingQueue
    • ConcurrentLinkedQueue

    These collections provide thread-safe access with better scalability than legacy synchronized collections.

    Thread Safety

    Thread-safe classes prevent inconsistent results when accessed by multiple threads simultaneously.

    Examples:

    • ConcurrentHashMap
    • AtomicInteger
    • BlockingQueue

    Non-thread-safe:

    • ArrayList
    • HashMap
    • LinkedList

    Real-World Example: Banking Transaction

    code
    public synchronized void withdraw(
    double amount){
    if(balance >= amount){
    balance -= amount;
    }
    }

    Synchronization prevents two users from withdrawing the same balance simultaneously.

    Real-World Example: Spring Boot

    code
    ExecutorService executor =
    Executors.newFixedThreadPool(10);
    executor.submit(() ->
    sendEmail()
    );

    Common use cases:

    • Email notifications
    • PDF generation
    • Report generation
    • Background jobs

    Best Practices

    Prefer ExecutorService

    Avoid manually creating threads for production applications.

    Minimize Synchronization

    Synchronize only critical sections to reduce contention.

    Prefer Atomic Classes

    Use AtomicInteger, AtomicLong, and similar classes for simple counters.

    Shutdown Executor Services

    Always call:

    code
    executor.shutdown();

    or shutdownNow() when appropriate.

    Avoid Shared Mutable State

    Prefer immutable objects or thread-safe data structures whenever possible.

    Common Mistakes

    Calling run() Instead of start()

    run() executes on the current thread.

    start() creates a new thread.

    Forgetting to Release Locks

    Always release ReentrantLock in a finally block.

    Overusing Synchronization

    Excessive synchronization reduces scalability.

    Using HashMap in Concurrent Applications

    Use ConcurrentHashMap instead.

    Ignoring InterruptedException

    Handle interruption properly instead of swallowing it.

    Hands-on Exercise

    Create a Java program that:

    1. Creates a thread by extending Thread.
    2. Creates another thread using Runnable.
    3. Uses a lambda expression to start a thread.
    4. Executes tasks using an ExecutorService.
    5. Returns a value using Callable and Future.
    6. Demonstrates a race condition.
    7. Fixes the race condition using synchronized.
    8. Uses AtomicInteger instead of synchronization.
    9. Creates an asynchronous pipeline using CompletableFuture.
    10. Uses a ConcurrentHashMap in a multi-threaded scenario.

    Summary

    Java Multithreading enables applications to execute multiple tasks concurrently, improving responsiveness and throughput. Modern Java applications rely heavily on the ExecutorService, CompletableFuture, atomic classes, concurrent collections, and the java.util.concurrent package rather than manually managing threads. Understanding synchronization, thread safety, race conditions, and asynchronous programming is essential for building scalable enterprise systems.

    Key Takeaways

    • A process can contain multiple threads.
    • Threads share heap memory but have separate stacks.
    • Prefer implementing Runnable over extending Thread.
    • Use Callable when a task must return a result.
    • Synchronization protects shared mutable state.
    • volatile provides visibility but not atomicity.
    • ReentrantLock offers advanced locking capabilities.
    • Atomic classes provide efficient thread-safe operations.
    • Use ExecutorService instead of manually creating threads.
    • CompletableFuture simplifies asynchronous programming.
    • ConcurrentHashMap is preferred over Hashtable for concurrent access.

    Professional Interview Questions

    1What is the difference between a Process and a Thread?

    Professional Answer

    A process is an independent program with its own memory space and system resources. A thread is the smallest unit of execution within a process. Threads belonging to the same process share heap memory and resources but maintain separate stacks and program counters. Threads are lightweight and less expensive to create than processes.

    Follow-up Questions

    • What resources are shared between threads?
    • Why are threads considered lightweight?

    Interview Tip: Remember: Process → Independent Program, Thread → Lightweight Execution Unit.

    2What is the difference between Runnable and Callable?

    Professional Answer

    Runnable represents a task that does not return a result and cannot throw checked exceptions. Callable represents a task that returns a value and may throw checked exceptions. Callable is executed using an ExecutorService and returns a Future, which can be used to retrieve the computation result.

    Follow-up Questions

    • Can Runnable return a value?
    • Which interface works with Future?

    Interview Tip: A quick memory trick: Runnable → No Return, Callable → Returns Result.

    3What is the difference between synchronized, volatile, and AtomicInteger?

    Professional Answer

    synchronized provides mutual exclusion, ensuring that only one thread executes a critical section at a time while also providing visibility guarantees. volatile guarantees that updates to a variable are immediately visible to other threads but does not make compound operations atomic. AtomicInteger performs atomic operations such as incrementing and decrementing without requiring explicit synchronization, making it an efficient choice for thread-safe counters.

    Follow-up Questions

    • Can volatile prevent race conditions?
    • Why is AtomicInteger often faster than using synchronized for counters?
    • When would you choose ReentrantLock over synchronized?

    Interview Tip: Remember: volatile → Visibility, synchronized → Mutual Exclusion + Visibility, AtomicInteger → Lock-Free Atomic Operations.

    Ready to mark this lesson complete?Track your journey across the entire course.