Java Multithreading


Multithreading in Java is a process of executing multiple threads simultaneously to maximize CPU utilization. Each thread runs independently but shares the same memory space.

"Thread is a lightweight sub-process, the smallest unit of processing."


🔹 History

Version

Feature Added

Java 1.0

Basic Thread class, Runnable interface

Java 1.2

ThreadGroup enhancements

Java 1.5

java.util.concurrent package (Executor framework, locks, atomic variables, etc.)

Java 1.6+

Performance improvements in concurrency

Java 1.7

Fork/Join framework

Java 1.8

CompletableFuture, Lambda expressions in Runnable, Callable, etc.

Java 9+

Flow API (Reactive Streams), enhancements in CompletableFuture

Java 19

Virtual Threads (Project Loom Preview)


🔹 Thread-Related Classes and Interfaces

Class / Interface

Purpose

Thread

Direct class to create and control threads

Runnable

Functional interface to define thread task

Callable<V>

Like Runnable but returns result and can throw exceptions

Future<V>

Represents the result of an asynchronous computation

ExecutorService

Manages and executes tasks asynchronously

Executors

Factory class for thread pools

ThreadPoolExecutor

Core class behind Executors

CompletableFuture

Async programming model with chaining support


🔹 Thread Lifecycle

NEW -> RUNNABLE -> RUNNING -> WAITING / TIMED_WAITING / BLOCKED -> TERMINATED
  • NEW: Thread object created but not started.
  • RUNNABLE: Eligible to run, waiting for CPU.
  • RUNNING: Actively executing.
  • BLOCKED: Waiting for monitor lock (e.g., synchronized block).
  • WAITING: Indefinitely waiting for another thread to notify.
  • TIMED_WAITING: Waiting with timeout (e.g., sleepjoinwait(timeout)).
  • TERMINATED: Thread has finished execution.


🔹 Thread Class Methods

Method

Description

Real-time Use Case

start()

Begins thread execution

Launching background jobs like sending emails

run()

Contains thread code (called by start)

Logic for file processing

sleep(ms)

Pauses thread for milliseconds

Rate-limiting API calls

join()

Waits for another thread to finish

Ensure file is downloaded before processing

setName() / getName()

Set/get thread name

Logging & debugging

setPriority()

Set execution priority (1-10)

Prioritize UI over background tasks

interrupt()

Interrupts waiting or sleeping thread

Cancel long-running tasks

isAlive()

Checks if thread is still running

Polling mechanism to monitor status

yield()

Pause current thread and let others run

Used in thread scheduling experiments


Additional Useful Thread Class Methods 

Method

Description

Real-Time Use Case

currentThread()

Returns reference to currently executing thread

Useful in logging current thread name in multi-threaded logs

isInterrupted()

Checks if thread has been interrupted (without clearing flag)

Gracefully shutdown a long-running thread

interrupted() (static)

Checks and clears the interrupted status

Cancel processing if thread was interrupted

holdsLock(Object obj) (static)

Checks if current thread holds lock on given object

Used in advanced synchronization debugging

checkAccess()

Throws SecurityException if current thread can’t modify the thread

Security check in sandboxed environments

getState()

Returns enum Thread.State (NEW, RUNNABLE, etc.)

Monitoring thread health in admin dashboards

getId()

Returns thread’s ID

Correlating logs or tracking in thread pools

getStackTrace()

Returns stack trace elements of thread

Debugging and live thread dump analysis

getThreadGroup()

Returns thread's group

Used in structured thread grouping (e.g., per module)

setDaemon(true/false)

Sets as daemon thread

Background cleanup tasks that shouldn't block exit

isDaemon()

Checks if it's a daemon thread

Decide if shutdown hook should wait or not



🔹 Examples: Thread Class Methods 


• start() – Begin Execution in a Separate Thread

Use Case: Launching a background job (e.g., sending email)

class EmailSender extends Thread {
    public void run() {
        System.out.println("Sending email in background...");
    }
}
public class StartExample {
    public static void main(String[] args) {
        EmailSender emailThread = new EmailSender();
        emailThread.start(); // starts a new thread
        System.out.println("Main thread continues...");
    }
}


• run() – Defines the Thread Logic (usually invoked via start())

Use Case: File processing logic

class FileProcessor extends Thread {
    public void run() {
        System.out.println("Processing file in thread: " + Thread.currentThread().getName());
    }
}
public class RunExample {
    public static void main(String[] args) {
        FileProcessor processor = new FileProcessor();
        processor.run(); // runs on main thread, not recommended
    }
}

⚠️ Always use .start() unless you want synchronous execution on main thread.

• sleep(ms) – Pause Execution Temporarily

Use Case: Rate limiting API calls

public class SleepExample {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 5; i++) {
            System.out.println("Calling API " + i);
            Thread.sleep(1000); // wait 1 sec between calls
        }
    }
}

• join() – Wait for a Thread to Finish

Use Case: Ensure a file is downloaded before processing

class Downloader extends Thread {
    public void run() {
        try {
            Thread.sleep(2000); // simulate download
            System.out.println("Download complete");
        } catch (InterruptedException e) {
            System.out.println("Download interrupted");
        }
    }
}
public class JoinExample {
    public static void main(String[] args) throws InterruptedException {
        Downloader d = new Downloader();
        d.start();
        d.join(); // wait for download to finish
        System.out.println("Now processing the file...");
    }
}

• getName()/setName() – Name the Thread

Use Case: Debugging logs with thread names

public class NameExample {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("Running in: " + Thread.currentThread().getName());
        });
        t.setName("Logger-Thread");
        t.start();
    }
}

• setPriority() – Control Thread Priority (1–10)

Use Case: Give higher priority to UI thread

public class PriorityExample {
    public static void main(String[] args) {
        Thread low = new Thread(() -> System.out.println("Low priority"));
        Thread high = new Thread(() -> System.out.println("High priority"));
        low.setPriority(Thread.MIN_PRIORITY);
        high.setPriority(Thread.MAX_PRIORITY);
        low.start();
        high.start();
    }
}

⚠️ Priority behavior depends on JVM/OS, not always guaranteed.


• interrupt() – Signal Thread to Stop (esp. if sleeping or waiting)

Use Case: Cancel a long-running task

class LongTask extends Thread {
    public void run() {
        try {
            System.out.println("Task running...");
            Thread.sleep(5000); // long sleep
            System.out.println("Finished Task");
        } catch (InterruptedException e) {
            System.out.println("Task was interrupted!");
        }
    }
}
public class InterruptExample {
    public static void main(String[] args) throws InterruptedException {
        LongTask task = new LongTask();
        task.start();
        Thread.sleep(1000);
        task.interrupt(); // cancel the task
    }
}

• isAlive() – Check If Thread Is Still Running

Use Case: Monitor long process

public class IsAliveExample {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            try { Thread.sleep(2000); } catch (Exception e) {}
        });
        t.start();
        while (t.isAlive()) {
            System.out.println("Still running...");
            Thread.sleep(500);
        }
        System.out.println("Thread finished");
    }
}

• yield() – Hint to Scheduler to Let Other Threads Run

Use Case: Yield in CPU-bound thread experiments

public class YieldExample {
    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " : " + i);
                Thread.yield(); // suggest pause to scheduler
            }
        };
        Thread t1 = new Thread(task, "T1");
        Thread t2 = new Thread(task, "T2");
        t1.start();
        t2.start();
    }
}


🔹 Creating Threads – 3 Ways

(a) Extending Thread class

class MyThread extends Thread { public void run() { System.out.println("Thread running: " + Thread.currentThread().getName()); } }

(b) Implementing Runnable interface

Runnable task = () -> System.out.println("Runnable running!"); new Thread(task).start();

(c) Implementing Callable<V> with ExecutorService interfaces

Callable<String> task = () -> { Thread.sleep(1000); return "Task done!"; }; ExecutorService es = Executors.newSingleThreadExecutor(); Future<String> result = es.submit(task); System.out.println(result.get()); es.shutdown();


🔹 Real-Time Use Cases of Multithreading

Use Case

Description

Web Server

Handle each request in a separate thread

UI + Background

UI thread for user interaction, background threads for heavy processing

File Downloader

Download multiple files concurrently

Video Streaming

Decode and buffer concurrently

Scheduled Tasks

Reminders, health checks, cleanup jobs

Gaming

Separate threads for rendering, AI, sound

Banking

Async transaction processing & logging



🔹 Executor Framework

Executor Framework provides a high-level replacement for managing Threads manually.

It uses thread pools to efficiently manage the execution of multiple asynchronous tasks.

Real-Time Use Cases

Use Case

Description

Web Server

Handle multiple incoming HTTP requests concurrently

Background Jobs

Process tasks like sending emails, generating reports

Parallel Data Processing

Perform heavy computation in parallel threads

Task Scheduling

Run recurring jobs like monitoring or auto-saving

Key Components

Interface/Class

Description

Executor

Basic interface with execute(Runnable) method

ExecutorService

Extends Executor with lifecycle methods: submit(), shutdown()

ScheduledExecutorService

Executes tasks after a delay or periodically

ThreadPoolExecutor

Most powerful customizable thread pool

Executors

Factory class for creating executor instances

Key Executor Types from Executors Factory

Method

Description

Executors.newFixedThreadPool(n)

Fixed-size thread pool

Executors.newCachedThreadPool()

Grows/shrinks threads based on demand

Executors.newSingleThreadExecutor()

Single worker thread

Executors.newScheduledThreadPool(n)

Fixed pool with scheduling support

Basic Examples

Example – 1 : ExecutorService with submit()

import java.util.concurrent.*; public class ExecutorExample { public static void main(String[] args) throws InterruptedException, ExecutionException { ExecutorService executor = Executors.newFixedThreadPool(3); // 3 threads in pool Callable<String> task = () -> { Thread.sleep(1000); return "Task completed by " + Thread.currentThread().getName(); }; Future<String> result = executor.submit(task); System.out.println("Result: " + result.get()); // blocks until result is available executor.shutdown(); // graceful shutdown } }

Example – 2 : Process multiple customer requests concurrently

import java.util.concurrent.*; class CustomerRequest implements Runnable { private final String customerName; public CustomerRequest(String customerName) { this.customerName = customerName; } @Override public void run() { System.out.println("Processing request for " + customerName + " on " + Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException ignored) {} System.out.println("Completed request for " + customerName); } } public class WebServerSimulator { public static void main(String[] args) { ExecutorService service = Executors.newFixedThreadPool(4); // 4 concurrent customers for (int i = 1; i <= 10; i++) { service.submit(new CustomerRequest("Customer " + i)); } service.shutdown(); // Don't accept new tasks } }

Output:

Processing request for Customer 1 on pool-1-thread-1 Processing request for Customer 2 on pool-1-thread-2 ... Completed request for Customer 1

Example – 3 : ScheduledExecutorService – Periodic Tasks (e.g., Auto Save, Cron Jobs)

import java.util.concurrent.*; public class ScheduledJobExample { public static void main(String[] args) { ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); Runnable task = () -> System.out.println("Auto-saving at " + java.time.LocalTime.now()); scheduler.scheduleAtFixedRate(task, 0, 5, TimeUnit.SECONDS); // initial delay 0s, then every 5s } }

Example – 4 : Custom ThreadPoolExecutor (Monitoring Thread Usage)

import java.util.concurrent.*; public class CustomThreadPool { public static void main(String[] args) { ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, 4, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2) ); for (int i = 1; i <= 10; i++) { int taskId = i; executor.execute(() -> { System.out.println("Running task " + taskId + " on " + Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException ignored) {} }); } executor.shutdown(); } }

Example – 5 : A complete Spring Boot mini project that demonstrates:

  • Using @Async for asynchronous methods
  • Custom Executor configuration
  • Exposing a REST endpoint that runs tasks in the background using the Executor Framework

1. Project Structure

spring-async-demo/

|── AsyncDemoApplication.java

|── config/

|    |── AsyncConfig.java

|── controller/

|    |── TaskController.java

|── service/

   |── TaskService.java


2. pom.xml – Add Spring Boot Dependencies

<project xmlns="http://maven.apache.org/POM/4.0.0" ...>

  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example</groupId>

  <artifactId>spring-async-demo</artifactId>

  <version>0.0.1-SNAPSHOT</version>

  <parent>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-parent</artifactId>

    <version>3.2.0</version>

  </parent>


  <dependencies>

    <dependency>

      <groupId>org.springframework.boot</groupId>

      <artifactId>spring-boot-starter-web</artifactId>

    </dependency>

  </dependencies>

</project>


3. AsyncDemoApplication.java

package com.example.asyncdemo;


import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.scheduling.annotation.EnableAsync;


@SpringBootApplication

@EnableAsync  // 👈 Enables async execution

public class AsyncDemoApplication {

    public static void main(String[] args) {

        SpringApplication.run(AsyncDemoApplication.class, args);

    }

}


4. AsyncConfig.java – Custom ThreadPool Executor

package com.example.asyncdemo.config;


import org.springframework.context.annotation.*;

import org.springframework.scheduling.annotation.EnableAsync;

import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;


@Configuration

public class AsyncConfig {


    @Bean(name = "taskExecutor")

    public Executor taskExecutor() {

        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        executor.setCorePoolSize(3);

        executor.setMaxPoolSize(5);

        executor.setQueueCapacity(10);

        executor.setThreadNamePrefix("Async-Worker-");

        executor.initialize();

        return executor;

    }

}


5. TaskService.java – Long-Running Async Method

package com.example.asyncdemo.service;


import org.springframework.scheduling.annotation.Async;

import org.springframework.stereotype.Service;


@Service

public class TaskService {


    @Async("taskExecutor")  // 👈 Executes in background thread

    public void processTask(String taskName) {

        System.out.println("Started: " + taskName + " on thread " + Thread.currentThread().getName());

        try {

            Thread.sleep(5000);  // Simulate long-running job

        } catch (InterruptedException e) {

            Thread.currentThread().interrupt();

        }

        System.out.println("Completed: " + taskName);

    }

}


6. TaskController.java – Trigger Async Method via REST

package com.example.asyncdemo.controller;


import com.example.asyncdemo.service.TaskService;

import org.springframework.web.bind.annotation.*;


@RestController

@RequestMapping("/api/tasks")

public class TaskController {


    private final TaskService taskService;


    public TaskController(TaskService taskService) {

        this.taskService = taskService;

    }


    @PostMapping("/{name}")

    public String triggerTask(@PathVariable String name) {

        taskService.processTask(name);

        return "Task '" + name + "' started!";

    }

}


7. Run and Test

Start the application and hit:

curl -X POST http://localhost:8080/api/tasks/report1


You’ll see:

Task 'report1' started!


And in the console:

Started: report1 on thread Async-Worker-1

... (after 5 sec)

Completed: report1


You can hit the endpoint multiple times and see it run concurrently on different threads.


8. Summary

Feature

Description

@EnableAsync

Enables async processing in Spring

@Async

Marks method to run in background

ThreadPoolTaskExecutor

Manages reusable threads

REST Endpoint

Starts async task without blocking


Raw Thread vs Executor Framework

Feature / Aspect

Raw Thread (Thread, Runnable)

Executor Framework (ExecutorService, Executors)

Definition

Low-level abstraction for creating and managing a thread

High-level API for managing asynchronous task execution

Thread Management

Manual (start, stop, join)

Automatically managed using thread pools

Scalability

Poor (creates new thread for every task)

Excellent (reuses threads from pool)

Resource Utilization

Expensive (overhead of creating many threads)

Efficient (threads are reused and managed)

Code Simplicity

Complex to manage thread lifecycle, error handling

Cleaner and maintainable using task submission (submit(), invokeAll())

Task Submission

Directly using new Thread(task).start()

Submit via executor.execute() or executor.submit()

Result Handling

No return values (without hacks like callbacks)

Supports Future<T> and Callable for result and exception handling

Scheduling Support

Not available

Supported via ScheduledExecutorService

Shutdown Mechanism

Manual thread interruption or join

Graceful shutdown with shutdown(), awaitTermination()

Error Handling

Must manually handle exceptions inside run() method

Can capture via Future.get() or invokeAll()

Use Case

Simple, short-lived tasks (e.g., quick prototypes)

Production-ready systems, concurrent APIs, background jobs, etc.

Thread Naming/Tracking

Manual naming and monitoring

Advanced management and monitoring possible (ThreadPoolExecutor)

Built-in Thread Pools

No

Yes — newFixedThreadPool(), newCachedThreadPool(), etc.

Advanced Features

Not available

Available — scheduling, futures, lifecycle mgmt


Raw Thread vs Executor Framework Comparison Example

Raw Thread:

new Thread(() -> { System.out.println("Task running on " + Thread.currentThread().getName()); }).start();

Executor Framework:

ExecutorService executor = Executors.newFixedThreadPool(2); executor.submit(() -> System.out.println("Task on " + Thread.currentThread().getName())); executor.shutdown();

Criteria

Best Choice

Lightweight prototypes

Raw Thread

Scalable, production-grade apps

Executor Framework

Need for task result/failure tracking

Executor Framework

Repeating/scheduled tasks

Executor Framework (ScheduledExecutorService)


Best Practices – Executor Framework 

  • Always shut down executor using shutdown() or shutdownNow() to release threads.

  • Use bounded queues (e.g., ArrayBlockingQueue) to avoid OutOfMemoryError.

  • Avoid newCachedThreadPool() in production—uses unbounded threads.

  • Use Callable + Future to get results and handle exceptions.

  • Prefer Executors.newFixedThreadPool() for CPU-bound tasks.

  • Use ScheduledExecutorService for delayed or periodic tasks.

  • Handle exceptions inside tasks to avoid silent failures.

  • Set thread names via custom ThreadFactory for easier debugging.

  • Choose rejection policies wisely (Abort, CallerRuns, etc.).

  • Avoid long blocking operations in thread pool tasks.

  • Separate pools for long-running and short tasks to prevent starvation.

  • Use CompletionService for processing results as they complete.

  • Monitor thread pool stats (getPoolSize(), getActiveCount()).

  • Leverage CompletableFuture for clean, async task composition.


🔹 CompletableFuture

What is CompletableFuture in Java?

CompletableFuture is a feature-rich, non-blocking, and asynchronous programming API introduced in Java 8 (part of java.util.concurrent).


It represents a future result of an asynchronous computation — like a promise that will complete later, either successfully with a result or with an exception.

Key Features
  • Runs tasks asynchronously in the background.

  • Allows callback chaining using thenApply(), thenAccept(), etc.

  • Supports parallel execution, exception handling, and composing multiple tasks.

  • Can be manually completed.

  • Supports functional programming style.


Real-World Analogy

Imagine ordering food online:

  • You place an order → this returns a CompletableFuture.

  • While the order is being prepared, you can do other work.

  • Once the food is ready, you're notified and can consume it (like using thenAccept()).


Basic Example
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { return "Hello"; }); String result = future.thenApply(greeting -> greeting + " World").join(); System.out.println(result); // Output: Hello World

Common Use Cases

Scenario

How CompletableFuture Helps

Web apps

Make parallel API/database calls and combine responses

Microservices

Call multiple services concurrently and combine results

File processing

Upload and process files asynchronously

Data pipelines

Trigger steps in an async, event-driven manner


When to Use
  • To perform non-blocking background tasks.

  • When you want to chain multiple tasks together.

  • If you're dealing with concurrent API calls, processing pipelines, or event-driven programming.

  • When you want better performance and scalability without blocking threads.


Example: Fetch Product Details Concurrently

Scenario: In an e-commerce app, when a user views a product, you want to simultaneously fetch:

  1. Product Info (name, price, stock)

  2. Product Reviews

  3. Seller Details

  4. Shipping Cost Estimate

👉 These can run in parallel and then be combined into a single response.

Using CompletableFuture for Parallel API Calls

import java.util.concurrent.*; import java.util.*; public class ProductService { ExecutorService executor = Executors.newFixedThreadPool(4); // thread pool public CompletableFuture<String> fetchProductInfo(String productId) { return CompletableFuture.supplyAsync(() -> { delay(1000); return "Product: iPhone 15 Pro"; }, executor); } public CompletableFuture<String> fetchReviews(String productId) { return CompletableFuture.supplyAsync(() -> { delay(1200); return "Reviews: ⭐⭐⭐⭐☆"; }, executor); } public CompletableFuture<String> fetchSellerDetails(String productId) { return CompletableFuture.supplyAsync(() -> { delay(800); return "Seller: Apple Store"; }, executor); } public CompletableFuture<String> fetchShippingEstimate(String productId) { return CompletableFuture.supplyAsync(() -> { delay(900); return "Shipping: ₹100 in 2 days"; }, executor); } public void getCompleteProductDetails(String productId) { CompletableFuture<String> productInfo = fetchProductInfo(productId); CompletableFuture<String> reviews = fetchReviews(productId); CompletableFuture<String> seller = fetchSellerDetails(productId); CompletableFuture<String> shipping = fetchShippingEstimate(productId); CompletableFuture<Void> allOf = CompletableFuture.allOf(productInfo, reviews, seller, shipping); allOf.thenRun(() -> { try { System.out.println("🛒 Final Product Page"); System.out.println(productInfo.get()); System.out.println(reviews.get()); System.out.println(seller.get()); System.out.println(shipping.get()); } catch (Exception e) { e.printStackTrace(); } }); executor.shutdown(); } private void delay(long ms) { try { Thread.sleep(ms); } catch (InterruptedException ignored) {} } public static void main(String[] args) { new ProductService().getCompleteProductDetails("P123"); } }

Output (within ~1.2 sec total):

Final Product Page Product: iPhone 15 Pro Reviews: ⭐⭐⭐⭐☆ Seller: Apple Store Shipping: ₹100 in 2 days

👉 All 4 APIs ran concurrently using CompletableFuture.supplyAsync(), improving performance.


Benefits of CompletableFuture in Real Use Cases

Feature

Benefit

supplyAsync()

Run logic in background

CompletableFuture.allOf()

Wait for all tasks to complete

thenRun(), thenApply()

Compose and chain logic

exceptionally()

Handle errors gracefully

Non-blocking

Main thread is free, improves throughput

Other Real-World Scenarios

Use Case

Description

Microservices Aggregation

Combine data from multiple microservices in parallel

Search Engine

Query multiple indexes (title, content, tags) concurrently

Banking

Fetch account summary, recent transactions, and credit score at once

Analytics

Parallel ETL stages or dashboard widget calculations

News Feed

Pull posts, comments, likes in parallel for real-time feed rendering


Example: Integrate CompletableFuture with a Spring Boot application for a real-time parallel API aggregation use case — like loading a product page with multiple service calls in parallel.

Use Case: E-Commerce Product Page

REST endpoint /api/products/{id} loads:

  1. Product Info

  2. Reviews

  3. Seller Details

  4. Shipping Estimate

👉 All done in parallel using CompletableFuture.

1. Project Structure
spring-async-completable/ |── AsyncCompletableApp.java |── controller/ | └── ProductController.java |── service/ | └── ProductService.java |── model/ | └── ProductDetails.java

2. pom.xml 
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>

3. AsyncCompletableApp.java
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; @SpringBootApplication @EnableAsync // Enables async method execution public class AsyncCompletableApp { public static void main(String[] args) { SpringApplication.run(AsyncCompletableApp.class, args); } }

4. ProductDetails.java
package com.example.model; public class ProductDetails { public String info; public String reviews; public String seller; public String shipping; public ProductDetails(String info, String reviews, String seller, String shipping) { this.info = info; this.reviews = reviews; this.seller = seller; this.shipping = shipping; } }

5. ProductService.java
package com.example.service; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.util.concurrent.CompletableFuture; @Service public class ProductService { @Async public CompletableFuture<String> getProductInfo(String id) { sleep(1000); return CompletableFuture.completedFuture("Product: iPhone 15 Pro"); } @Async public CompletableFuture<String> getReviews(String id) { sleep(800); return CompletableFuture.completedFuture("Reviews: ⭐⭐⭐⭐☆"); } @Async public CompletableFuture<String> getSeller(String id) { sleep(600); return CompletableFuture.completedFuture("Seller: Apple Store"); } @Async public CompletableFuture<String> getShipping(String id) { sleep(700); return CompletableFuture.completedFuture("Shipping: ₹100 in 2 days"); } private void sleep(long ms) { try { Thread.sleep(ms); } catch (InterruptedException ignored) {} } }

6. ProductController.java
package com.example.controller; import com.example.model.ProductDetails; import com.example.service.ProductService; import org.springframework.web.bind.annotation.*; import java.util.concurrent.*; @RestController @RequestMapping("/api/products") public class ProductController { private final ProductService productService; public ProductController(ProductService productService) { this.productService = productService; } @GetMapping("/{id}") public CompletableFuture<ProductDetails> getProductDetails(@PathVariable String id) { CompletableFuture<String> infoFuture = productService.getProductInfo(id); CompletableFuture<String> reviewsFuture = productService.getReviews(id); CompletableFuture<String> sellerFuture = productService.getSeller(id); CompletableFuture<String> shippingFuture = productService.getShipping(id); return CompletableFuture.allOf(infoFuture, reviewsFuture, sellerFuture, shippingFuture) .thenApply(voidResult -> { try { return new ProductDetails( infoFuture.get(), reviewsFuture.get(), sellerFuture.get(), shippingFuture.get() ); } catch (Exception e) { throw new RuntimeException("Error fetching product details", e); } }); } }

7. Sample Output (JSON Response)

Hit:

GET http://localhost:8080/api/products/P123

Output:

{ "info": "Product: iPhone 15 Pro", "reviews": "Reviews: ⭐⭐⭐⭐☆", "seller": "Seller: Apple Store", "shipping": "Shipping: ₹100 in 2 days" }
👉 All services were invoked in parallel, and the response time is ~1 second (not 4 seconds!).

8. Benefits

Benefit

Description

Non-blocking

REST thread isn't blocked while fetching

Fast & Efficient

Tasks run concurrently

Clean Composition

Easy to compose multiple futures

Production Ready

Works great for microservice aggregation


Quick Comparison: Runnable vs Callable vs CompletableFuture

Feature

Runnable

Callable<T>

CompletableFuture

Returns value

No

Yes

Yes

Throws Exception

No

Yes

Yes

Java Version

Java 1.0

Java 5

Java 8

Async support

Limited

Via Future

Fluent API chaining


🔹 Thread-Safety and Synchronization

Thread-safety and synchronization are critical concepts in multi-threaded applications, especially in web apps where multiple requests can access shared data concurrently.

What is Thread-Safety?

A thread-safe class or method behaves correctly when multiple threads access it simultaneously, even without external synchronization.

What is Synchronization?

Synchronization is a mechanism to control access to shared resources to avoid data inconsistency (race conditions).

It can be achieved using:

  • synchronized keyword

  • Lock interface

  • Concurrent classes like ConcurrentHashMap, AtomicInteger

Real-Time Use Case (Spring Boot)

Let’s say we are building an API to track the number of visits to your website. We want to ensure the count is thread-safe.

Example: Page Visit Counter — Thread-Safe

🔴 Problem (Non-thread-safe)

@Service public class VisitCounterService { private int count = 0; public int incrementAndGet() { count++; return count; } }

Issue: Multiple threads can update count at the same time → Race condition → Incorrect values.

Solution 1: Using synchronized (basic synchronization)

@Service public class VisitCounterService { private int count = 0; public synchronized int incrementAndGet() { count++; return count; } }

Solution 2: Using AtomicInteger (non-blocking, thread-safe)

import java.util.concurrent.atomic.AtomicInteger; @Service public class VisitCounterService { private final AtomicInteger count = new AtomicInteger(0); public int incrementAndGet() { return count.incrementAndGet(); } }

Solution 3: Using ReentrantLock (flexible locking)

import java.util.concurrent.locks.ReentrantLock; @Service public class VisitCounterService { private int count = 0; private final ReentrantLock lock = new ReentrantLock(); public int incrementAndGet() { lock.lock(); try { count++; return count; } finally { lock.unlock(); } } }

Spring Boot Controller Example

@RestController @RequestMapping("/api/visits") public class VisitController { @Autowired private VisitCounterService visitCounterService; @GetMapping("/increment") public ResponseEntity<String> incrementVisit() { int count = visitCounterService.incrementAndGet(); return ResponseEntity.ok("Visit Count: " + count); } }

Test It Concurrently

Hit the /api/visits/increment endpoint with concurrent HTTP requests using Postman Runner, JMeter, or a custom script.

Summary

Approach

Thread-Safe

Blocking

Use Case

synchronized

Yes

Yes

Simple use cases, small methods

AtomicInteger

Yes

No

High concurrency counters, stats tracking

ReentrantLock

Yes

Controlled

When fine-grained control is needed


🔹 Fork/Join Framework

A Java framework (Java 7+) to split big tasks into smaller ones, run them in parallel, and combine the results — based on divide-and-conquer.

Use Case:  E.g. Fast summing of a large array using parallel threads.

Example: Simple Fork/Join (Sum from 1 to 20)

import java.util.concurrent.*; class SimpleSumTask extends RecursiveTask<Integer> { int[] nums; int start, end; SimpleSumTask(int[] nums, int start, int end) { this.nums = nums; this.start = start; this.end = end; } @Override protected Integer compute() { if (end - start <= 5) { // small task, compute directly int sum = 0; for (int i = start; i < end; i++) sum += nums[i]; return sum; } else { int mid = (start + end) / 2; SimpleSumTask left = new SimpleSumTask(nums, start, mid); SimpleSumTask right = new SimpleSumTask(nums, mid, end); left.fork(); // run in parallel return right.compute() + left.join(); // combine results } } } public class SimpleForkJoinDemo { public static void main(String[] args) { int[] numbers = new int[20]; for (int i = 0; i < 20; i++) numbers[i] = i + 1; // [1, 2, ..., 20] ForkJoinPool pool = new ForkJoinPool(); SimpleSumTask task = new SimpleSumTask(numbers, 0, numbers.length); int total = pool.invoke(task); System.out.println("Total Sum: " + total); // Output: 210 } }

Output:

Total Sum: 210


🔹 Virtual Threads (Java 19+) – Lightweight Concurrency

Virtual threads are lightweight, low-overhead threads introduced in Java 19 (preview) and made stable in Java 21 as part of Project Loom. They are managed by the JVM, not the OS, making them ideal for massively concurrent applications.

Key Features

  • Much cheaper than platform (OS) threads.
  • Thousands to millions can be created without exhausting system resources.
  • Ideal for I/O-bound tasks (REST APIs, chat servers, etc.).
  • Simplifies concurrency (no need for complex async frameworks).

Syntax Example
public class VirtualThreadDemo { public static void main(String[] args) { Runnable task = () -> { System.out.println("Running in: " + Thread.currentThread()); }; // Start a virtual thread (Java 21 or preview in Java 19/20) Thread.startVirtualThread(task); } }

Output: Running in: VirtualThread[#27]/runnable@ForkJoinPool


Virtual Threads Real-World Use Cases

Use Case

Why Virtual Threads Help

REST APIs

Can handle thousands of concurrent requests with simpler code

Chat Servers

Each connection as a virtual thread

Load testing

Simulate millions of concurrent users

Microservices

Easy thread-per-request model


Traditional vs Virtual Threads

Feature

Traditional Threads

Virtual Threads

Managed by

OS

JVM

Cost

High

Low

Blocking

Wastes resources

Non-blocking with Thread.sleep, I/O

Scalability

Limited

High (millions of threads)

API Changes

None needed

Uses existing Thread / Runnable

Example: Spring Boot (Java 21+) example using virtual threads to handle thousands of concurrent HTTP requests with traditional blocking code — but backed by lightweight, scalable virtual threads.

Requirements: 

  • Java 21+
  • Spring Boot 3.2+
  • Enable virtual thread support in your app

1️⃣ Spring Boot App (Main Class)
package com.example.virtualthreads; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor; import java.time.Duration; import java.util.concurrent.Executors; @SpringBootApplication public class VirtualThreadApp { public static void main(String[] args) { SpringApplication.run(VirtualThreadApp.class, args); } // Use virtual threads as task executor @Bean public TaskExecutor applicationTaskExecutor() { return new ConcurrentTaskExecutor(Executors.newVirtualThreadPerTaskExecutor()); } @Bean public RestTemplateBuilder restTemplateBuilder() { return new RestTemplateBuilder().setConnectTimeout(Duration.ofSeconds(2)).setReadTimeout(Duration.ofSeconds(2)); } }
2️⃣ Controller: Simulate Blocking Logic with Virtual Threads
package com.example.virtualthreads; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class VirtualThreadController { @GetMapping("/hello") public String hello() throws InterruptedException { // Simulate a blocking I/O operation (e.g., DB call) Thread.sleep(100); // This will NOT block an OS thread! return "Hello from virtual thread: " + Thread.currentThread(); } }
3️⃣ Test It with Concurrency

Use a simple load test (e.g., Apache Bench or wrk):

ab -n 1000 -c 200 http://localhost:8080/hello

Or simulate from your browser hitting /hello many times.

Why This Works Well

Feature

Traditional Threads

Virtual Threads (Here)

Thread cost

Heavy (OS-managed)

Light (JVM-managed)

Blocking sleep()

Blocks OS thread

JVM parks thread efficiently

Scaling connections

Limited (100s)

Massive (100,000+)

Spring complexity

Needs async/reactive

Same old @RestController!



👉 With Java 21 virtual threads + Spring Boot 3.2+, you can handle massive concurrency (like 100k connections)
using simple, blocking code — no more CompletableFuture, reactive streams, or async servlets needed.






🔹 Object Class Methods Used in Java Multithreading

These methods enable thread coordination and inter-thread communication via intrinsic locks (monitor) in Java.

Method

Description

Real-Time Use Case

wait()

Causes the current thread to wait until another thread invokes notify()/notifyAll() on the same object

Used in Producer-Consumer, task queues, throttling systems

wait(long timeout)

Waits for a specified time or until notified

Used in time-bound operations like polling with timeout

wait(long timeout, int nanos)

Waits with nanosecond precision

Rarely used directly; mostly in low-level timing precision

notify()

Wakes up a single thread waiting on the object

Waking up a blocked consumer after producer inserts item

notifyAll()

Wakes up all threads waiting on the object’s monitor

Used when all threads may proceed after a condition is met



Examples: Object class methods used in java multithreading

• wait() Pause Thread Until Notified

Use Case: Producer-Consumer style waiting

public class WaitExample { public static void main(String[] args) { Object lock = new Object(); Thread waiter = new Thread(() -> { synchronized (lock) { System.out.println("Waiting for notification..."); try { lock.wait(); // releases lock and waits } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Notified and resumed!"); } }); waiter.start(); try { Thread.sleep(1000); } catch (InterruptedException ignored) {} Thread notifier = new Thread(() -> { synchronized (lock) { System.out.println("Sending notification..."); lock.notify(); // wakes up one waiting thread } }); notifier.start(); } }
• notify() – Wake Up One Waiting Thread

Use Case: Resuming a single consumer from multiple waiting threads

public class NotifyExample { public static void main(String[] args) { Object lock = new Object(); Runnable waiter = () -> { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " waiting..."); try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " resumed!"); } }; Thread t1 = new Thread(waiter, "Waiter-1"); Thread t2 = new Thread(waiter, "Waiter-2"); t1.start(); t2.start(); try { Thread.sleep(2000); } catch (InterruptedException ignored) {} new Thread(() -> { synchronized (lock) { System.out.println("Notifier waking one..."); lock.notify(); // only one waiter resumes } }).start(); } }
• notifyAll() – Wake Up All Waiting Threads

Use Case: Releasing all threads after condition is met (like reloading cache)

public class NotifyAllExample { public static void main(String[] args) { Object lock = new Object(); Runnable waiter = () -> { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " waiting..."); try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " resumed!"); } }; for (int i = 1; i <= 3; i++) { new Thread(waiter, "Waiter-" + i).start(); } try { Thread.sleep(2000); } catch (InterruptedException ignored) {} new Thread(() -> { synchronized (lock) { System.out.println("Notifier waking ALL..."); lock.notifyAll(); // all waiters resume } }).start(); } }

When to Use These:

Method

Use Case

wait()

Thread should pause until condition met

notify()

Resume one thread (e.g., single consumer)

notifyAll()

Resume all threads (e.g., cache reload complete)