Java Multithreading




Part 1: Fundamentals, Thread Lifecycle, Thread Creation and Synchronization



Introduction

Modern applications often perform multiple tasks simultaneously. A web application may handle user requests, process payments, send emails, update databases, and generate reports at the same time.

Java Multithreading enables concurrent execution of multiple tasks within a single process, improving responsiveness, throughput, and resource utilization.

Common Use Cases

  • Processing HTTP requests

  • File uploads and downloads

  • Background jobs

  • Email notifications

  • Real-time chat applications

  • Video streaming platforms

  • Banking systems

  • Event-driven applications


What is a Thread?

A thread is the smallest unit of execution within a process.

Multiple threads can execute concurrently while sharing the same memory space and application resources.

Use Cases

  • Request processing

  • Background tasks

  • File processing

  • Notifications

  • Report generation


Process vs Thread

Process

A process is an independent program in execution.

Examples

  • Chrome Browser

  • IntelliJ IDEA

  • MySQL Database

Thread

A thread is a lightweight execution unit inside a process.

Multiple threads share:

  • Heap Memory

  • Application Resources

  • Database Connections

  • Network Connections

Comparison

FeatureProcessThread
MemorySeparateShared
Creation CostHighLow
Resource UsageHighLow
CommunicationExpensiveFast
Context SwitchingSlowFast

Benefits of Multithreading

  • Improved Responsiveness

    • Applications remain responsive while background operations execute.

    • Example: Users can continue browsing while a file uploads.

  • Better CPU Utilization

    • Modern CPUs contain multiple cores.

    • Multithreading helps utilize available CPU cores efficiently.

  • Increased Throughput

    • Multiple requests can be processed simultaneously.

    • Example: A web server can process hundreds of requests concurrently.

  • Reduced Waiting Time

    • Database calls, API calls, and file operations can execute in parallel.


Thread Lifecycle

A thread goes through multiple states during its lifetime.

NEW
 ↓
RUNNABLE
 ↓
RUNNING
 ↓
BLOCKED / WAITING / TIMED_WAITING
 ↓
TERMINATED
  • NEW: Thread object created but not started.

  • RUNNABLE: Ready for execution and waiting for CPU allocation.

  • RUNNING: Currently executing.

  • BLOCKED: Waiting to acquire a lock.

  • WAITING: Waiting indefinitely for another thread.

  • TIMED_WAITING: Waiting for a specified period.

  • TERMINATED: Execution completed.

Use Cases

  • Thread dump analysis

  • Performance tuning

  • Production troubleshooting

  • Concurrency debugging


Creating Threads

Java provides multiple ways to create threads.

1. Extending Thread Class

Creates a thread by extending the Thread class.

Example

class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println("Thread Running");
    }
}

new MyThread().start();

Use Cases

  • Learning thread basics

  • Small utility applications


2. Implementing Runnable Interface

Runnable separates task logic from thread creation.

Example

Runnable task =
        () -> System.out.println("Runnable Running");

new Thread(task).start();

Advantages

  • Preferred approach

  • Better code design

  • Supports lambda expressions

  • Allows extending another class

Use Cases

  • Background jobs

  • Logging systems

  • Notification processing


3. Implementing Callable Interface

Callable can return a value and throw checked exceptions.

Example

Callable<String> task =
        () -> "Task Completed";

Advantages

  • Returns results

  • Supports exception handling

  • Integrates with Executor Framework

Use Cases

  • Database operations

  • API calls

  • Report generation


Thread Class Methods

start()

Starts a new thread and internally invokes run().

Use Cases

  • Background processing

  • Parallel execution

run()

Contains thread execution logic.

Important Notes

  • Calling run() directly does not create a new thread.

  • Always use start() for concurrent execution.

sleep()

Pauses execution for a specified duration.

Example

Thread.sleep(1000);

Use Cases

  • Rate limiting

  • Retry mechanisms

  • Polling systems

join()

Waits for another thread to complete.

Example

Downloader d = new Downloader();

d.start();

d.join();

System.out.println("Processing File");

Use Cases

  • File processing workflows

  • Task dependency management

interrupt()

Requests interruption of a running thread.

Example

task.start();

task.interrupt();

Use Cases

  • Graceful shutdown

  • Task cancellation

isAlive()

Checks whether a thread is still running.

Use Cases

  • Monitoring task completion

  • Health checks

yield()

Suggests the scheduler switch execution to another thread.

Use Cases

  • Thread scheduling experiments

  • Concurrency testing


Thread Safety

Thread Safety ensures shared resources behave correctly when accessed by multiple threads simultaneously.

Non Thread-Safe Example

public class Counter {

    private int count = 0;

    public int increment() {
        return ++count;
    }
}

Problem

  • Multiple threads may update count simultaneously.

  • Results become inconsistent.

  • This issue is called a Race Condition.

Example

Expected Result:

10000

Actual Result:

8734

Use Cases

  • Banking systems

  • Inventory management

  • Order processing

  • Payment systems


Synchronization

Synchronization controls access to shared resources and prevents data inconsistency.

synchronized Keyword

Allows only one thread at a time to execute a synchronized block or method.

Example

public synchronized int increment() {
    return ++count;
}

Advantages

  • Easy to implement

  • Prevents race conditions

  • Built into Java

Use Cases

  • Shared counters

  • Account balance updates

  • Inventory management


Atomic Classes

Atomic classes provide lock-free and thread-safe operations on variables.

Common Atomic Classes

  • AtomicInteger

  • AtomicLong

  • AtomicBoolean

  • AtomicReference

Example

private AtomicInteger counter =
        new AtomicInteger();

public int increment() {
    return counter.incrementAndGet();
}

Use Cases

  • Request counters

  • Statistics collection

  • Metrics monitoring

  • Sequence generation

Advantages

  • Thread-safe

  • Non-blocking

  • Better performance than synchronized


volatile Keyword

The volatile keyword ensures updates made by one thread become immediately visible to other threads.

Example

private volatile boolean running = true;

Use Cases

  • Application shutdown flags

  • Status indicators

  • Feature toggles

Important Notes

  • Provides visibility.

  • Does not provide atomicity.

  • Does not replace synchronization.


ReentrantLock

ReentrantLock provides advanced locking capabilities compared to synchronized.

Example

private final ReentrantLock lock =
        new ReentrantLock();

lock.lock();

try {
    // business logic
} finally {
    lock.unlock();
}

Advantages

  • Fair locking support

  • Try-lock functionality

  • Interruptible locking

  • Better control over locking behavior

Use Cases

  • Resource management

  • High-concurrency applications

  • Complex locking scenarios


ReadWriteLock

ReadWriteLock allows multiple readers but only one writer at a time.

Example

ReadWriteLock lock =
        new ReentrantReadWriteLock();

Advantages

  • Better performance for read-heavy workloads

  • Reduced lock contention

Use Cases

  • Cache systems

  • Configuration management

  • Read-heavy applications


Thread Communication

wait()

Causes the current thread to wait until notified.

Example

synchronized(lock) {
    lock.wait();
}

Use Cases

  • Producer Consumer Pattern

  • Task Queues

notify()

Wakes up one waiting thread.

Example

synchronized(lock) {
    lock.notify();
}

Use Cases

  • Resume blocked consumer

  • Signal task completion

notifyAll()

Wakes up all waiting threads.

Example

synchronized(lock) {
    lock.notifyAll();
}

Use Cases

  • Shared resource availability

  • Broadcast notifications


Common Concurrency Problems

  • Race Condition

    • Multiple threads modify shared data simultaneously causing inconsistent results.

  • Deadlock

    • Two or more threads wait indefinitely for each other.

  • Starvation

    • A thread never gets sufficient CPU resources because other threads continuously get priority.

  • Livelock

    • Threads remain active but continuously react to each other without making progress.


Real-World Applications of Multithreading

Web Servers

  • Spring Boot

  • Tomcat

  • Jetty

Banking Systems

  • Transaction processing

  • Audit logging

  • Notification services

File Processing Systems

  • Parallel file uploads

  • Data transformation

  • Report generation

Messaging Systems

  • Kafka consumers

  • RabbitMQ consumers

  • Event processing

Streaming Applications

  • Video processing

  • Audio processing

  • Live streaming





Part 2: Executor Framework, Thread Pools, Future, CompletableFuture and Concurrent Collections



Why Not Create Threads Manually?

Creating threads using new Thread() works for small applications but becomes difficult to manage in enterprise systems.

Problems

  • High thread creation cost

  • Increased memory usage

  • Difficult lifecycle management

  • Poor scalability

  • No thread reuse

Example

new Thread(() -> processOrder()).start();

new Thread(() -> sendEmail()).start();

new Thread(() -> generateInvoice()).start();

Creating thousands of threads this way can impact performance.

The Executor Framework solves these problems.


Executor Framework

The Executor Framework provides a higher-level API for managing and executing asynchronous tasks.

Instead of creating threads manually, tasks are submitted to a thread pool.

Benefits

  • Thread reuse

  • Better resource management

  • Improved performance

  • Easier scalability

  • Centralized task execution

Architecture

Task
 ↓
Executor
 ↓
ExecutorService
 ↓
Thread Pool
 ↓
Worker Threads

Example

ExecutorService executor =
        Executors.newFixedThreadPool(5);

executor.submit(() ->
        System.out.println("Task Executed"));

executor.shutdown();

Use Cases

  • REST APIs

  • Batch processing

  • Message processing

  • Background jobs

  • Notification services


Executor vs ExecutorService

Executor

Basic interface for executing tasks.

Example

Executor executor =
        Executors.newSingleThreadExecutor();

executor.execute(() ->
        System.out.println("Running"));

ExecutorService

Provides advanced functionality.

Features

  • Submit tasks

  • Shutdown thread pools

  • Return results

  • Manage task lifecycle


Thread Pools

A Thread Pool is a collection of reusable worker threads.

Benefits

  • Reduces thread creation overhead

  • Improves performance

  • Controls resource usage

  • Supports high concurrency


Fixed Thread Pool

Creates a fixed number of threads.

Example

ExecutorService executor =
        Executors.newFixedThreadPool(5);

Use Cases

  • REST APIs

  • Order processing

  • Payment processing

  • Microservices


Cached Thread Pool

Creates threads as needed and reuses idle threads.

Example

ExecutorService executor =
        Executors.newCachedThreadPool();

Use Cases

  • Short-lived tasks

  • Lightweight asynchronous operations

  • Burst workloads


Single Thread Executor

Uses a single worker thread.

Example

ExecutorService executor =
        Executors.newSingleThreadExecutor();

Use Cases

  • Sequential processing

  • Logging systems

  • Event processing


Scheduled Thread Pool

Executes tasks after a delay or periodically.

Example

ScheduledExecutorService scheduler =
        Executors.newScheduledThreadPool(2);

scheduler.scheduleAtFixedRate(
        () -> System.out.println("Running"),
        0,
        5,
        TimeUnit.SECONDS);

Use Cases

  • Health checks

  • Cleanup jobs

  • Scheduled reports

  • Periodic notifications


ThreadPoolExecutor

ThreadPoolExecutor provides complete control over thread pool behavior.

Example

ThreadPoolExecutor executor =
        new ThreadPoolExecutor(
                2,
                4,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(2));

Important Parameters

  • Core Pool Size

  • Maximum Pool Size

  • Keep Alive Time

  • Work Queue

Use Cases

  • High-volume APIs

  • Batch processing

  • Enterprise applications


Real-World Thread Pool Example

Consider a banking application receiving customer requests.

Example

ExecutorService service =
        Executors.newFixedThreadPool(4);

for (int i = 1; i <= 10; i++) {

    service.submit(
            () -> System.out.println(
                    Thread.currentThread()
                            .getName()));
}

Benefits

  • Reuses threads

  • Handles multiple requests efficiently

  • Reduces thread creation overhead


Callable Interface

Callable is similar to Runnable but can return a value and throw checked exceptions.

Example

Callable<String> task =
        () -> "Order Processed";

Advantages

  • Returns values

  • Supports exceptions

  • Works with Future

Use Cases

  • Database queries

  • API calls

  • Report generation

  • Data aggregation


Future

Future represents the result of an asynchronous computation.

Example

ExecutorService executor =
        Executors.newSingleThreadExecutor();

Future<String> future =
        executor.submit(
                () -> "Task Completed");

System.out.println(
        future.get());

Common Methods

  • get()

  • get(timeout, unit)

  • cancel()

  • isDone()

  • isCancelled()

Example

future.get(
        5,
        TimeUnit.SECONDS);

Limitations

  • get() blocks the current thread.

  • Difficult to combine multiple asynchronous tasks.


CompletableFuture

CompletableFuture simplifies asynchronous and non-blocking programming.

Benefits

  • Non-blocking execution

  • Task chaining

  • Better error handling

  • Parallel processing support


supplyAsync()

Used when a result is required.

Example

CompletableFuture<String> future =
        CompletableFuture.supplyAsync(
                () -> "Hello");

Use Cases

  • API calls

  • Database queries

  • Background calculations


runAsync()

Used when no result is required.

Example

CompletableFuture<Void> future =
        CompletableFuture.runAsync(
                () -> System.out.println(
                        "Running"));

Use Cases

  • Logging

  • Notifications

  • Background cleanup


thenApply()

Transforms the result of a previous task.

Example

CompletableFuture<String> future =
        CompletableFuture
                .supplyAsync(() -> "Java")
                .thenApply(
                        value -> value + " 21");

Use Cases

  • DTO mapping

  • Data transformation

  • Response enrichment


thenAccept()

Consumes the result without returning another value.

Example

CompletableFuture
        .supplyAsync(() -> "Order")
        .thenAccept(System.out::println);

Use Cases

  • Logging

  • Notifications

  • Auditing


thenRun()

Executes another task after completion.

Example

CompletableFuture
        .runAsync(() -> processOrder())
        .thenRun(() -> sendEmail());

Use Cases

  • Post-processing actions

  • Notifications

  • Workflow completion


thenCompose()

Chains dependent asynchronous tasks.

Example

CompletableFuture<String> future =
        CompletableFuture
                .supplyAsync(() -> "User")
                .thenCompose(
                        user ->
                                CompletableFuture
                                .supplyAsync(
                                        () -> user + " Details"));

Use Cases

  • User → Orders

  • Orders → Payments

  • Workflow processing


thenCombine()

Combines results from independent tasks.

Example

CompletableFuture<String> user =
        CompletableFuture.supplyAsync(
                () -> "User");

CompletableFuture<String> orders =
        CompletableFuture.supplyAsync(
                () -> "Orders");

user.thenCombine(
        orders,
        (u, o) -> u + " " + o);

Use Cases

  • Dashboard loading

  • API aggregation

  • Microservice communication


allOf()

Waits for all tasks to complete.

Example

CompletableFuture.allOf(
        future1,
        future2,
        future3);

Use Cases

  • Product Details

  • Inventory

  • Pricing

  • Reviews

Load everything in parallel before sending the response.


anyOf()

Returns when any task completes.

Example

CompletableFuture.anyOf(
        future1,
        future2,
        future3);

Use Cases

  • Multiple service providers

  • Fastest response wins

  • Fallback strategies


exceptionally()

Handles exceptions in asynchronous pipelines.

Example

CompletableFuture<String> future =
        CompletableFuture
                .supplyAsync(() -> {
                    throw new RuntimeException();
                })
                .exceptionally(
                        ex -> "Fallback Value");

Use Cases

  • Error recovery

  • Fallback responses

  • Resilience patterns


Real-World CompletableFuture Example

A Product Details page may require data from multiple services.

Services

  • Product Service

  • Inventory Service

  • Pricing Service

  • Review Service

Example

CompletableFuture<Product> product =
        fetchProduct();

CompletableFuture<Inventory> inventory =
        fetchInventory();

CompletableFuture<Price> price =
        fetchPrice();

CompletableFuture<Review> review =
        fetchReview();

CompletableFuture.allOf(
        product,
        inventory,
        price,
        review).join();

Benefits

  • Faster response times

  • Better scalability

  • Improved user experience


Concurrent Collections

Java provides thread-safe collections designed for concurrent access.

Benefits

  • Thread-safe

  • Better scalability

  • Reduced locking overhead


ConcurrentHashMap

Thread-safe alternative to HashMap.

Example

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

map.put("1", "Java");

Use Cases

  • Caching

  • Session storage

  • Metrics collection


CopyOnWriteArrayList

Creates a new copy of the collection on every write operation.

Example

CopyOnWriteArrayList<String> list =
        new CopyOnWriteArrayList<>();

Use Cases

  • Configuration data

  • Read-heavy applications

  • Listener registration


BlockingQueue

Thread-safe queue designed for Producer-Consumer scenarios.

Example

BlockingQueue<String> queue =
        new LinkedBlockingQueue<>();

queue.put("Task");

String task = queue.take();

Use Cases

  • Task scheduling

  • Message processing

  • Producer Consumer Pattern


ConcurrentLinkedQueue

Non-blocking thread-safe queue.

Example

ConcurrentLinkedQueue<String> queue =
        new ConcurrentLinkedQueue<>();

queue.offer("Task");

Use Cases

  • Event processing

  • High-throughput systems

  • Message queues


CountDownLatch

Allows one or more threads to wait until a set of operations completes.

Example

CountDownLatch latch =
        new CountDownLatch(3);

latch.countDown();

latch.await();

Use Cases

  • Parallel service startup

  • Batch processing

  • Integration testing


CyclicBarrier

Allows multiple threads to wait for each other before proceeding.

Example

CyclicBarrier barrier =
        new CyclicBarrier(3);

Use Cases

  • Parallel computations

  • Simulation systems

  • Multi-stage processing


Semaphore

Controls access to a limited number of resources.

Example

Semaphore semaphore =
        new Semaphore(5);

semaphore.acquire();

try {
    // business logic
} finally {
    semaphore.release();
}

Use Cases

  • Database connection pools

  • Rate limiting

  • Resource management


Executor Framework Best Practices

  • Prefer ExecutorService over raw threads.

  • Always shutdown thread pools.

  • Use FixedThreadPool for predictable workloads.

  • Use ScheduledExecutorService for scheduled jobs.

  • Avoid creating excessive threads.

  • Use CompletableFuture for async workflows.

  • Use Concurrent Collections for shared data.

  • Monitor thread pool utilization in production.





Part 3: Fork/Join Framework, Virtual Threads (Java 21) and Concurrency Best Practices



Evolution of Java Concurrency

Java concurrency has evolved significantly over the years.

Java 1.0
- Thread
- Runnable

Java 5
- Executor Framework
- Callable
- Future
- Concurrent Collections
- Atomic Classes

Java 7
- Fork/Join Framework

Java 8
- CompletableFuture
- Parallel Streams

Java 21
- Virtual Threads

Fork/Join Framework

The Fork/Join Framework was introduced in Java 7 for efficient parallel processing of large tasks.

It follows the Divide and Conquer approach.

Benefits

  • Efficient parallel processing

  • Better CPU utilization

  • Automatic workload balancing

  • Recursive task execution

Use Cases

  • Data analytics

  • Image processing

  • File scanning

  • Scientific calculations

  • Large dataset processing


ForkJoinPool

ForkJoinPool is the core implementation of the Fork/Join Framework.

Example

ForkJoinPool pool =
        new ForkJoinPool();

Responsibilities

  • Manage worker threads

  • Execute subtasks

  • Balance workload

  • Implement work stealing

Use Cases

  • CPU-intensive workloads

  • Parallel processing

  • Large computations


RecursiveTask

RecursiveTask is used when a task returns a result.

Example

class SumTask
        extends RecursiveTask<Integer> {

    @Override
    protected Integer compute() {

        // split task

        return result;
    }
}

Use Cases

  • Sum calculations

  • Data aggregation

  • Report generation

  • Analytics processing


RecursiveAction

RecursiveAction is used when no result is required.

Example

class FileProcessor
        extends RecursiveAction {

    @Override
    protected void compute() {

        // process files
    }
}

Use Cases

  • File processing

  • Batch updates

  • Data migration

  • Background cleanup


Complete Fork/Join Example

Suppose we want to calculate the sum of a large array.

Example

class SumTask extends RecursiveTask<Integer> {

    private int[] numbers;
    private int start;
    private int end;

    public SumTask(
            int[] numbers,
            int start,
            int end) {

        this.numbers = numbers;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {

        if (end - start <= 5) {

            int sum = 0;

            for (int i = start;
                 i < end;
                 i++) {

                sum += numbers[i];
            }

            return sum;
        }

        int middle =
                (start + end) / 2;

        SumTask left =
                new SumTask(
                        numbers,
                        start,
                        middle);

        SumTask right =
                new SumTask(
                        numbers,
                        middle,
                        end);

        left.fork();

        return right.compute()
                + left.join();
    }
}

Execution

int[] numbers =
        {1,2,3,4,5,6,7,8,9,10};

ForkJoinPool pool =
        new ForkJoinPool();

int total =
        pool.invoke(
                new SumTask(
                        numbers,
                        0,
                        numbers.length));

System.out.println(total);

Benefits

  • Parallel execution

  • Better CPU utilization

  • Automatic task splitting


Work Stealing Algorithm

Work Stealing is the key optimization behind the Fork/Join Framework.

When a worker thread becomes idle, it steals tasks from busy worker threads.

Example

Thread-1 : Busy
Thread-2 : Busy
Thread-3 : Idle

Thread-3 steals work
from Thread-1 or Thread-2

Benefits

  • Better throughput

  • Improved CPU utilization

  • Reduced idle time

  • Automatic load balancing


Parallel Streams

Parallel Streams internally use ForkJoinPool for parallel execution.

Example

List<Integer> numbers =
        Arrays.asList(
                1,2,3,4,5);

numbers.parallelStream()
       .forEach(
               System.out::println);

Use Cases

  • Data transformation

  • Analytics

  • Aggregation

  • Reporting

Important Notes

  • Best suited for CPU-intensive tasks.

  • Avoid for database calls.

  • Avoid for external API calls.


Virtual Threads (Java 21)

Virtual Threads are lightweight threads managed by the JVM instead of the operating system.

They were introduced as a stable feature in Java 21 under Project Loom.

Benefits

  • Lightweight

  • Low memory usage

  • Massive scalability

  • Simplified concurrent programming

Use Cases

  • REST APIs

  • Microservices

  • Database operations

  • Network communication

  • High-concurrency applications


Traditional Threads vs Virtual Threads

FeatureTraditional ThreadVirtual Thread
Managed ByOSJVM
Creation CostHighVery Low
Memory UsageHighLow
ScalabilityThousandsMillions
Blocking CallsExpensiveCheap
Context SwitchingExpensiveLightweight

Creating Virtual Threads

Example

Thread.startVirtualThread(
        () -> {
            System.out.println(
                Thread.currentThread());
        });

Alternative Approach

Thread thread =
        Thread.ofVirtual()
              .start(() ->
                      System.out.println(
                              "Running"));

Use Cases

  • API requests

  • Database calls

  • File operations

  • Message processing


Virtual Thread Executor

Java provides a dedicated ExecutorService for Virtual Threads.

Example

try (ExecutorService executor =
        Executors
         .newVirtualThreadPerTaskExecutor()) {

    executor.submit(
            () -> processOrder());
}

Benefits

  • One virtual thread per task

  • Massive scalability

  • Simplified concurrency

Use Cases

  • Microservices

  • High-volume APIs

  • Event-driven systems


Carrier Threads

Virtual Threads do not directly run on OS threads.

Instead, they are scheduled on a smaller number of platform threads called Carrier Threads.

Workflow

Virtual Threads
        |
        v
Carrier Threads
        |
        v
Operating System Threads

Benefits

  • Reduced resource consumption

  • Better scalability

  • Efficient scheduling


When to Use Virtual Threads

Recommended For

  • Database calls

  • REST API calls

  • File operations

  • Network communication

  • Messaging systems

Example

Request
   |
Database Call
   |
External API Call
   |
Response

Virtual Threads excel in I/O-heavy applications.


When Not to Use Virtual Threads

Avoid For

  • Video encoding

  • Image rendering

  • Scientific computations

  • Machine learning calculations

  • CPU-intensive analytics

Better Choice

  • Fork/Join Framework

  • Parallel Streams

  • Executor Framework


Choosing the Right Concurrency Model

ScenarioRecommended Approach
Small ApplicationThread / Runnable
REST APIsExecutor Framework
Database OperationsVirtual Threads
API AggregationCompletableFuture
Scheduled JobsScheduledExecutorService
Parallel ComputationFork/Join Framework
High-Concurrency SystemsVirtual Threads
Data ProcessingFork/Join Framework

Common Multithreading Mistakes

  • Creating excessive threads manually.

  • Forgetting to shutdown ExecutorService.

  • Using synchronized everywhere.

  • Ignoring race conditions.

  • Blocking CompletableFuture using get().

  • Sharing mutable objects without synchronization.

  • Using parallel streams for database calls.

  • Ignoring InterruptedException.


Multithreading Best Practices

  • Prefer ExecutorService over raw threads.

  • Prefer CompletableFuture for async workflows.

  • Use Concurrent Collections for shared data.

  • Use Atomic classes for counters.

  • Use Virtual Threads for I/O-intensive workloads.

  • Keep shared mutable state minimal.

  • Release locks in finally blocks.

  • Monitor thread pool utilization.

  • Handle exceptions properly.

  • Avoid unnecessary synchronization.


Common Questions

Runnable vs Callable

  • Runnable does not return a value.

  • Callable returns a value and supports exceptions.

synchronized vs ReentrantLock

  • synchronized is simple and built-in.

  • ReentrantLock provides advanced locking features.

AtomicInteger vs synchronized

  • AtomicInteger is non-blocking.

  • synchronized uses locking.

Future vs CompletableFuture

  • Future supports basic asynchronous execution.

  • CompletableFuture supports chaining and composition.

CountDownLatch vs CyclicBarrier

  • CountDownLatch is one-time use.

  • CyclicBarrier can be reused.

Fork/Join Framework vs Executor Framework

  • Fork/Join is designed for recursive parallel computation.

  • Executor Framework is designed for general task execution.

Traditional Thread vs Virtual Thread

  • Traditional threads are OS-managed.

  • Virtual Threads are JVM-managed and highly scalable.


Real-World Architecture Example

Modern microservices often combine multiple concurrency approaches.

Request Flow

Client Request
      |
Virtual Thread
      |
-----------------------
|         |           |
User     Order     Payment
Service  Service   Service
|         |           |
-----------------------
      |
CompletableFuture
      |
Response

Technologies Used

  • Virtual Threads → Handle incoming requests

  • CompletableFuture → Parallel service calls

  • Executor Framework → Background jobs

  • ConcurrentHashMap → Shared caching

  • Fork/Join Framework → Large dataset processing

Benefits

  • High scalability

  • Better responsiveness

  • Efficient resource utilization

  • Improved throughput