0% found this document useful (0 votes)
20 views41 pages

Java Threading & Spring Boot Guide

This document is a comprehensive guide on Java threading and Spring Boot concurrency, covering topics such as single-threading, multi-threading, Future, CompletableFuture, and ExecutorService. It includes explanations, implementation examples, best practices, and real-world applications to help developers understand and effectively utilize concurrency in Java. Additionally, it provides interview questions and answers to reinforce learning.

Uploaded by

Puneet Prakash
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
20 views41 pages

Java Threading & Spring Boot Guide

This document is a comprehensive guide on Java threading and Spring Boot concurrency, covering topics such as single-threading, multi-threading, Future, CompletableFuture, and ExecutorService. It includes explanations, implementation examples, best practices, and real-world applications to help developers understand and effectively utilize concurrency in Java. Additionally, it provides interview questions and answers to reinforce learning.

Uploaded by

Puneet Prakash
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Complete Guide to Java Threading and Spring

Boot Concurrency
Table of Contents
1. Single-Threading
2. Multi-Threading
3. Future
4. CompletableFuture
5. ExecutorService
6. Virtual Threads (Java 19+)
7. Spring-Managed Threads
8. Evolution of Java Concurrency
9. Comparison Table
10. Real-World Mini-Projects
11. Interview Questions & Answers

Single-Threading
What it is
Single-threading means executing one task at a time in sequence. Each task
must complete before the next one begins. In Java, this is the default behavior
of the main thread.
Under the hood: The JVM allocates one thread of execution with its own
stack memory. Tasks are executed sequentially on the CPU core assigned to
this thread.

Why and when to use it


• Simple applications with minimal I/O operations
• CPU-intensive tasks that don’t benefit from parallelization
• Sequential dependencies where order matters
• Resource-constrained environments

How to implement it
// Simple single-threaded example
public class SingleThreadedExample {
public static void main(String[] args) {
[Link]("Task 1 starting...");
performTask("Task 1", 2000);

[Link]("Task 2 starting...");
performTask("Task 2", 1500);

1
[Link]("Task 3 starting...");
performTask("Task 3", 1000);

[Link]("All tasks completed!");


}

private static void performTask(String taskName, int durationMs) {


try {
[Link](durationMs); // Simulating work
[Link](taskName + " completed in " + durationMs + "ms");
} catch (InterruptedException e) {
[Link]().interrupt();
}
}
}

Best Practices
• Keep tasks short and non-blocking
• Handle exceptions properly
• Avoid blocking operations in critical paths
• Use for simple, sequential workflows

Multi-Threading
What it is
Multi-threading allows multiple threads to execute concurrently, potentially on
different CPU cores. Each thread has its own stack but shares heap memory.
Under the hood: The JVM creates multiple threads, each with 1MB stack
space (default). The OS scheduler distributes these threads across available
CPU cores.

Why and when to use it


• I/O-intensive operations (file reading, network calls)
• CPU-intensive tasks that can be parallelized
• Background processing while keeping UI responsive
• Handling multiple client requests simultaneously

How to implement it
Basic Threading

2
public class BasicMultiThreading {
public static void main(String[] args) {
// Method 1: Extending Thread
Thread thread1 = new CustomThread("Thread-1");

// Method 2: Implementing Runnable


Thread thread2 = new Thread(new CustomRunnable("Thread-2"));

// Method 3: Lambda expression


Thread thread3 = new Thread(() -> {
performTask("Lambda-Thread", 1000);
});

[Link]();
[Link]();
[Link]();

// Wait for all threads to complete


try {
[Link]();
[Link]();
[Link]();
} catch (InterruptedException e) {
[Link]().interrupt();
}

[Link]("All threads completed!");


}

static class CustomThread extends Thread {


private String name;

public CustomThread(String name) {


[Link] = name;
}

@Override
public void run() {
performTask(name, 2000);
}
}

static class CustomRunnable implements Runnable {


private String name;

public CustomRunnable(String name) {

3
[Link] = name;
}

@Override
public void run() {
performTask(name, 1500);
}
}

private static void performTask(String taskName, int durationMs) {


try {
[Link](taskName + " started on " + [Link]().getName())
[Link](durationMs);
[Link](taskName + " completed!");
} catch (InterruptedException e) {
[Link]().interrupt();
}
}
}

Advanced: Thread Synchronization


public class ThreadSynchronization {
private static int counter = 0;
private static final Object lock = new Object();

public static void main(String[] args) throws InterruptedException {


List<Thread> threads = new ArrayList<>();

// Create 10 threads that increment counter


for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
incrementCounter();
}
});
[Link](thread);
[Link]();
}

// Wait for all threads


for (Thread thread : threads) {
[Link]();
}

[Link]("Final counter value: " + counter);

4
// Should be 10,000 with proper synchronization
}

private static void incrementCounter() {


synchronized (lock) {
counter++;
}
}
}

Best Practices
• Always call start(), never run() directly
• Use join() to wait for thread completion
• Handle InterruptedException properly
• Avoid shared mutable state or use synchronization
• Use thread-safe collections when needed

Future
What it is
Future represents the result of an asynchronous computation. It provides meth-
ods to check if the computation is complete, wait for completion, and retrieve
the result.
Under the hood: Future is an interface that acts as a placeholder for a value
that will be available later. It’s typically returned by ExecutorService when
submitting tasks.

Why and when to use it


• Asynchronous task execution with result retrieval
• Non-blocking operations where you need the result later
• Timeout handling for long-running operations
• Task cancellation capabilities

How to implement it
Basic Future Usage
import [Link].*;

public class FutureExample {


public static void main(String[] args) {
ExecutorService executor = [Link](3);

5
try {
// Submit callable tasks that return values
Future<String> future1 = [Link](new ApiCallTask("[Link]
Future<String> future2 = [Link](new ApiCallTask("[Link]
Future<Integer> future3 = [Link](new CalculationTask(100));

// Do other work while tasks execute


[Link]("Tasks submitted, doing other work...");
[Link](500);

// Get results (blocking calls)


try {
String result1 = [Link](5, [Link]);
String result2 = [Link](5, [Link]);
Integer result3 = [Link](5, [Link]);

[Link]("Results: " + result1 + ", " + result2 + ", " + result3);


} catch (TimeoutException e) {
[Link]("Task timed out!");
[Link](true);
[Link](true);
[Link](true);
}

} catch (InterruptedException | ExecutionException e) {


[Link]();
} finally {
[Link]();
}
}

static class ApiCallTask implements Callable<String> {


private String url;

public ApiCallTask(String url) {


[Link] = url;
}

@Override
public String call() throws Exception {
// Simulate API call
[Link](2000);
return "Response from " + url;
}
}

6
static class CalculationTask implements Callable<Integer> {
private int input;

public CalculationTask(int input) {


[Link] = input;
}

@Override
public Integer call() throws Exception {
// Simulate heavy calculation
[Link](1000);
return input * input;
}
}
}

Advanced: Future with Error Handling


public class AdvancedFutureExample {
public static void main(String[] args) {
ExecutorService executor = [Link](2);

List<Future<String>> futures = new ArrayList<>();

// Submit multiple tasks


for (int i = 1; i <= 5; i++) {
final int taskId = i;
Future<String> future = [Link](() -> {
if (taskId == 3) {
throw new RuntimeException("Task " + taskId + " failed!");
}
[Link](1000 * taskId);
return "Task " + taskId + " completed";
});
[Link](future);
}

// Process results as they complete


for (int i = 0; i < [Link](); i++) {
try {
String result = [Link](i).get();
[Link]("Success: " + result);
} catch (ExecutionException e) {
[Link]("Task " + (i + 1) + " failed: " + [Link]().getMessage
} catch (InterruptedException e) {
[Link]().interrupt();

7
break;
}
}

[Link]();
}
}

Best Practices
• Always use timeouts with get(timeout, unit)
• Handle ExecutionException and InterruptedException
• Cancel futures when no longer needed
• Use isDone() and isCancelled() for status checking

CompletableFuture
What it is
CompletableFuture is an enhanced Future that supports functional-style pro-
gramming with method chaining, composition, and better exception handling.
It implements both Future and CompletionStage interfaces.
Under the hood: Built on top of ForkJoinPool, it uses a callback-based ap-
proach with continuation passing style for non-blocking operations.

Why and when to use it


• Complex async workflows with dependencies between tasks
• Non-blocking programming with callbacks
• Functional composition of async operations
• Better error handling compared to raw Future

How to implement it
Basic CompletableFuture
import [Link];
import [Link];

public class CompletableFutureBasics {


public static void main(String[] args) {
// Creating CompletableFuture in different ways

// 1. Completed future
CompletableFuture<String> completed = [Link]("Hello");

8
// 2. Async supplier
CompletableFuture<String> async = [Link](() -> {
try {
[Link](1000);
return "Async result";
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});

// 3. Async runnable (no return value)


CompletableFuture<Void> runAsync = [Link](() -> {
[Link]("Background task executed");
});

// Chain operations
CompletableFuture<String> chained = async
.thenApply(result -> [Link]())
.thenApply(result -> "Processed: " + result);

// Get results
try {
[Link]([Link]());
[Link]([Link](3, [Link]));
[Link]();
} catch (Exception e) {
[Link]();
}
}
}

Advanced: Composition and Error Handling


public class AdvancedCompletableFuture {
public static void main(String[] args) {
// Complex async workflow
CompletableFuture<String> userFuture = fetchUser(123);
CompletableFuture<String> profileFuture = fetchUserProfile(123);

// Combine two independent futures


CompletableFuture<String> combinedFuture = userFuture
.thenCombine(profileFuture, (user, profile) ->
"User: " + user + ", Profile: " + profile);

// Chain dependent operations


CompletableFuture<String> processedFuture = userFuture

9
.thenCompose(user -> fetchUserPreferences(user))
.thenApply(preferences -> "Processed preferences: " + preferences)
.exceptionally(throwable -> {
[Link]("Error occurred: " + [Link]());
return "Default preferences";
});

// Handle both success and failure


CompletableFuture<String> handledFuture = fetchUser(456)
.handle((result, throwable) -> {
if (throwable != null) {
return "Error: " + [Link]();
}
return "Success: " + result;
});

// Wait for all to complete


CompletableFuture<Void> allOf = [Link](
combinedFuture, processedFuture, handledFuture
);

[Link](() -> {
try {
[Link]("Combined: " + [Link]());
[Link]("Processed: " + [Link]());
[Link]("Handled: " + [Link]());
} catch (Exception e) {
[Link]();
}
}).join();
}

private static CompletableFuture<String> fetchUser(int userId) {


return [Link](() -> {
// Simulate API call
try {
[Link](1000);
if (userId == 456) {
throw new RuntimeException("User not found");
}
return "User-" + userId;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}

10
private static CompletableFuture<String> fetchUserProfile(int userId) {
return [Link](() -> {
try {
[Link](800);
return "Profile-" + userId;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}

private static CompletableFuture<String> fetchUserPreferences(String user) {


return [Link](() -> {
try {
[Link](500);
return "Preferences for " + user;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
}

Best Practices
• Use method chaining for readable async workflows
• Handle exceptions with exceptionally() or handle()
• Use thenCompose() for dependent operations, thenCombine() for inde-
pendent ones
• Prefer supplyAsync() over manual thread creation
• Use custom executors for better control

ExecutorService
What it is
ExecutorService is a high-level interface for managing and controlling thread
execution. It provides thread pools, task scheduling, and lifecycle management.
Under the hood: Manages a pool of worker threads, uses blocking queues for
task submission, and handles thread lifecycle automatically.

Why and when to use it


• Thread pool management to avoid thread creation overhead

11
• Resource control to limit concurrent threads
• Task queuing when threads are busy
• Graceful shutdown of threading resources

How to implement it
Basic ExecutorService
import [Link].*;
import [Link];
import [Link];

public class ExecutorServiceExample {


public static void main(String[] args) {
// Different types of thread pools

// 1. Fixed thread pool


ExecutorService fixedPool = [Link](3);

// 2. Cached thread pool (creates threads as needed)


ExecutorService cachedPool = [Link]();

// 3. Single thread executor


ExecutorService singleExecutor = [Link]();

// 4. Scheduled executor
ScheduledExecutorService scheduledExecutor = [Link](2);

try {
// Submit tasks to fixed pool
List<Future<String>> futures = new ArrayList<>();

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


final int taskId = i;
Future<String> future = [Link](() -> {
[Link](1000 * taskId);
return "Task " + taskId + " completed by " + [Link]().getN
});
[Link](future);
}

// Collect results
for (Future<String> future : futures) {
[Link]([Link]());
}

12
// Schedule recurring task
[Link](() -> {
[Link]("Scheduled task executed at " + [Link](
}, 0, 2, [Link]);

[Link](6000); // Let scheduled task run a few times

} catch (Exception e) {
[Link]();
} finally {
// Proper shutdown
shutdownExecutor(fixedPool);
shutdownExecutor(cachedPool);
shutdownExecutor(singleExecutor);
shutdownExecutor(scheduledExecutor);
}
}

private static void shutdownExecutor(ExecutorService executor) {


[Link]();
try {
if (![Link](5, [Link])) {
[Link]();
}
} catch (InterruptedException e) {
[Link]();
[Link]().interrupt();
}
}
}

Custom ThreadPoolExecutor
public class CustomThreadPoolExample {
public static void main(String[] args) {
// Custom thread pool with specific parameters
ThreadPoolExecutor customExecutor = new ThreadPoolExecutor(
2, // core pool size
4, // maximum pool size
60L, // keep alive time
[Link], // time unit
new ArrayBlockingQueue<>(10), // work queue
new CustomThreadFactory(), // thread factory
new [Link]() // rejection policy
);

13
// Submit tasks
for (int i = 1; i <= 15; i++) {
final int taskId = i;
[Link](() -> {
try {
[Link]("Task " + taskId + " started by " +
[Link]().getName());
[Link](2000);
[Link]("Task " + taskId + " completed");
} catch (InterruptedException e) {
[Link]().interrupt();
}
});
}

// Monitor pool status


[Link]("Active threads: " + [Link]());
[Link]("Pool size: " + [Link]());
[Link]("Queue size: " + [Link]().size());

[Link]();
}

static class CustomThreadFactory implements ThreadFactory {


private int counter = 1;

@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "CustomWorker-" + counter++);
[Link](false);
return thread;
}
}
}

Best Practices
• Choose appropriate thread pool type for your use case
• Always shutdown executors properly
• Handle rejection policies appropriately
• Monitor thread pool metrics in production
• Use custom ThreadFactory for better thread naming

14
Virtual Threads (Java 19+)
What it is
Virtual threads are lightweight threads managed by the JVM rather than the
OS. They’re designed for high-throughput concurrent applications with millions
of threads.
Under the hood: Virtual threads are mapped to a small number of OS threads
(carrier threads) by the JVM. They’re suspended when blocked and resumed
when unblocked, allowing massive concurrency with minimal memory overhead.

Why and when to use it


• High-concurrency applications (millions of concurrent operations)
• I/O-intensive workloads where threads spend time waiting
• Microservices with many external API calls
• Web servers handling many concurrent requests

How to implement it
Basic Virtual Threads (Java 21+)
// Note: This requires Java 21+ for stable virtual threads
public class VirtualThreadsExample {
public static void main(String[] args) throws InterruptedException {
// Create virtual thread executor
try (ExecutorService executor = [Link]()) {

// Submit many lightweight tasks


List<Future<String>> futures = new ArrayList<>();

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


final int taskId = i;
Future<String> future = [Link](() -> {
try {
// Simulate I/O operation
[Link](1000);
return "Virtual task " + taskId + " on " + [Link]();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
[Link](future);
}

// Process first 10 results


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

15
try {
[Link]([Link](i).get());
} catch (Exception e) {
[Link]();
}
}

[Link]("Submitted 10,000 virtual threads successfully!");


}
}
}

Virtual Threads with Structured Concurrency


import [Link];

// Java 21+ structured concurrency (preview feature)


public class StructuredConcurrencyExample {

public String fetchUserData(int userId) throws Exception {


try (var scope = new [Link]()) {

// Start multiple related tasks


var userTask = [Link](() -> fetchUser(userId));
var profileTask = [Link](() -> fetchProfile(userId));
var preferencesTask = [Link](() -> fetchPreferences(userId));

// Wait for all to complete or any to fail


[Link](); // Wait for all tasks
[Link](); // Throw if any failed

// All succeeded, combine results


return combineUserData(
[Link](),
[Link](),
[Link]()
);
}
}

private String fetchUser(int userId) throws InterruptedException {


[Link](1000);
return "User-" + userId;
}

private String fetchProfile(int userId) throws InterruptedException {

16
[Link](800);
return "Profile-" + userId;
}

private String fetchPreferences(int userId) throws InterruptedException {


[Link](600);
return "Preferences-" + userId;
}

private String combineUserData(String user, String profile, String preferences) {


return [Link]("Combined: %s, %s, %s", user, profile, preferences);
}
}

Best Practices
• Use for I/O-intensive applications
• Avoid CPU-intensive tasks in virtual threads
• Don’t use ThreadLocal extensively (memory overhead)
• Use structured concurrency for related task groups
• Monitor carrier thread utilization

Spring-Managed Threads
What it is
Spring provides several mechanisms for async processing including @Async anno-
tation, TaskExecutor, and TaskScheduler. Spring manages the thread lifecycle
and configuration.
Under the hood: Spring uses AOP proxies to intercept @Async method calls
and execute them on configured thread pools.

Why and when to use it


• Spring Boot applications needing async processing
• Declarative async programming with annotations
• Integration with Spring ecosystem (transactions, security)
• Configuration-driven thread management

How to implement it
Basic @Async Setup
// Configuration
@Configuration
@EnableAsync

17
public class AsyncConfig {

@Bean(name = "taskExecutor")
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
[Link](2);
[Link](5);
[Link](100);
[Link]("Async-");
[Link](new [Link]());
[Link]();
return executor;
}

@Bean(name = "longRunningTaskExecutor")
public TaskExecutor longRunningTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
[Link](1);
[Link](3);
[Link](50);
[Link]("LongTask-");
[Link]();
return executor;
}
}

// Service class
@Service
public class AsyncService {

private static final Logger logger = [Link]([Link]);

@Async("taskExecutor")
public CompletableFuture<String> processDataAsync(String data) {
[Link]("Processing {} on thread {}", data, [Link]().getName());

try {
// Simulate processing
[Link](2000);
String result = "Processed: " + [Link]();
return [Link](result);
} catch (InterruptedException e) {
[Link]().interrupt();
return [Link](e);
}
}

18
@Async("longRunningTaskExecutor")
public void performBackgroundTask(String taskName) {
[Link]("Background task {} started on thread {}",
taskName, [Link]().getName());

try {
[Link](5000);
[Link]("Background task {} completed", taskName);
} catch (InterruptedException e) {
[Link]().interrupt();
[Link]("Background task {} interrupted", taskName);
}
}

@Async
public CompletableFuture<List<String>> processMultipleItems(List<String> items) {
return [Link](() -> {
return [Link]()
.map(item -> "Processed: " + item)
.collect([Link]());
});
}
}

Spring Boot Application


@SpringBootApplication
@EnableAsync
public class AsyncDemoApplication {

public static void main(String[] args) {


[Link]([Link], args);
}
}

@RestController
public class AsyncController {

@Autowired
private AsyncService asyncService;

@GetMapping("/process/{data}")
public CompletableFuture<ResponseEntity<String>> processData(@PathVariable String data)
return [Link](data)
.thenApply(result -> [Link](result))

19
.exceptionally(throwable ->
[Link](HttpStatus.INTERNAL_SERVER_ERROR)
.body("Error: " + [Link]()));
}

@PostMapping("/background-task")
public ResponseEntity<String> startBackgroundTask(@RequestBody String taskName) {
[Link](taskName);
return [Link]().body("Task started: " + taskName);
}

@PostMapping("/process-batch")
public CompletableFuture<ResponseEntity<List<String>>> processBatch(
@RequestBody List<String> items) {
return [Link](items)
.thenApply(results -> [Link](results));
}
}

Advanced: Custom Async Exception Handler


@Component
public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

private static final Logger logger = [Link](CustomAsyncExceptionHandler

@Override
public void handleUncaughtException(Throwable throwable, Method method, Object... params
[Link]("Async method {} failed with parameters {}",
[Link](), [Link](params), throwable);

// You could also send notifications, metrics, etc.


sendErrorNotification([Link](), throwable);
}

private void sendErrorNotification(String methodName, Throwable error) {


// Implementation for error notification
[Link]("Sending error notification for method: {}", methodName);
}
}

@Configuration
@EnableAsync
public class AsyncConfigWithExceptionHandler implements AsyncConfigurer {

@Override

20
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
[Link](3);
[Link](6);
[Link](100);
[Link]("SpringAsync-");
[Link]();
return executor;
}

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new CustomAsyncExceptionHandler();
}
}

Best Practices
• Configure appropriate thread pool sizes
• Use different executors for different task types
• Implement proper exception handling
• Avoid calling @Async methods from the same class
• Monitor thread pool metrics

Java Threading Guide - Part 2: Spring & Ad-


vanced Concepts
Spring-Managed Threads (Continued)
Advanced Spring Async Patterns
Event-Driven Async Processing
@Component
public class OrderEventHandler {

private static final Logger logger = [Link]([Link]);

@EventListener
@Async("orderProcessingExecutor")
public void handleOrderCreated(OrderCreatedEvent event) {
[Link]("Processing order {} asynchronously", [Link]());

try {
// Simulate order processing

21
[Link](3000);

// Send confirmation email


sendConfirmationEmail([Link]());

// Update inventory
updateInventory([Link]());

[Link]("Order {} processing completed", [Link]());


} catch (Exception e) {
[Link]("Failed to process order {}", [Link](), e);
}
}

@EventListener
@Async("notificationExecutor")
public void handlePaymentProcessed(PaymentProcessedEvent event) {
// Send notification asynchronously
sendPaymentNotification([Link](), [Link]());
}

private void sendConfirmationEmail(String orderId) throws InterruptedException {


[Link](1000);
[Link]("Confirmation email sent for order {}", orderId);
}

private void updateInventory(List<String> items) throws InterruptedException {


[Link](500);
[Link]("Inventory updated for items: {}", items);
}

private void sendPaymentNotification(String orderId, double amount) {


[Link]("Payment notification sent for order {} amount ${}", orderId, amount);
}
}

// Event classes
public class OrderCreatedEvent {
private String orderId;
private List<String> items;

// constructors, getters, setters


public OrderCreatedEvent(String orderId, List<String> items) {
[Link] = orderId;
[Link] = items;
}

22
public String getOrderId() { return orderId; }
public List<String> getItems() { return items; }
}

public class PaymentProcessedEvent {


private String orderId;
private double amount;

public PaymentProcessedEvent(String orderId, double amount) {


[Link] = orderId;
[Link] = amount;
}

public String getOrderId() { return orderId; }


public double getAmount() { return amount; }
}

Conditional Async Execution


@Service
public class ConditionalAsyncService {

@Value("${[Link]:true}")
private boolean asyncEnabled;

public CompletableFuture<String> processData(String data) {


if (asyncEnabled) {
return processDataAsync(data);
} else {
return [Link](processDataSync(data));
}
}

@Async
public CompletableFuture<String> processDataAsync(String data) {
try {
[Link](2000);
return [Link]("Async: " + [Link]());
} catch (InterruptedException e) {
[Link]().interrupt();
return [Link](e);
}
}

public String processDataSync(String data) {

23
try {
[Link](2000);
return "Sync: " + [Link]();
} catch (InterruptedException e) {
[Link]().interrupt();
throw new RuntimeException(e);
}
}
}

Evolution of Java Concurrency


Timeline and Key Improvements
1. Thread Class (Java 1.0 - 1996)
// Basic thread creation
Thread thread = new Thread(() -> {
[Link]("Hello from thread!");
});
[Link]();
Limitations: Manual lifecycle, resource leaks, poor scalability

2. ExecutorService (Java 5 - 2004)


// Thread pool management
ExecutorService executor = [Link](5);
Future<String> future = [Link](() -> "Hello");
String result = [Link]();
[Link]();
Improvements: Thread pooling, lifecycle management, better resource control

3. CompletableFuture (Java 8 - 2014)


// Functional async programming
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> "Hello")
.thenApply(String::toUpperCase)
.thenCompose(this::processString);
Improvements: Method chaining, better composition, exception handling

4. Virtual Threads (Java 19-21)


// Lightweight concurrency
try (var executor = [Link]()) {

24
// Can handle millions of concurrent tasks
for (int i = 0; i < 1_000_000; i++) {
[Link](() -> {
// I/O intensive work
});
}
}
Improvements: Massive concurrency, lower memory footprint, simpler pro-
gramming model

Comparison Table

Single- Multi- Virtual Spring


FeatureThreading Threading FutureCompletableFuture
ExecutorService
Threads @Async
Complexity
Very Medium Medium
High Medium Low Low
Low
Performance
Poor for Good Good Excellent Good Excellent Good
I/O
MemoryLow High High Medium Medium Very Medium
Us- Low
age
Scalability
Poor Limited LimitedGood Good Excellent Good
Error Simple Complex Basic Advanced Basic Simple Spring-
Han- managed
dling
Composability
N/A Poor LimitedExcellent Limited Good Limited
Learning
Easy Hard Medium Hard Medium Easy Easy
Curve
Best Simple CPU- Basic Complex General High Spring
Use tasks intensive async work- purpose I/O apps
Case flows

Real-World Mini-Projects
Project 1: API Response Aggregator
@Service
public class ApiAggregatorService {

private final RestTemplate restTemplate = new RestTemplate();

25
// Single-threaded approach (slow)
public AggregatedResponse fetchDataSingleThreaded(List<String> apiUrls) {
List<String> responses = new ArrayList<>();
long startTime = [Link]();

for (String url : apiUrls) {


try {
String response = [Link](url, [Link]);
[Link](response);
} catch (Exception e) {
[Link]("Error: " + [Link]());
}
}

long duration = [Link]() - startTime;


return new AggregatedResponse(responses, duration);
}

// CompletableFuture approach (fast)


public CompletableFuture<AggregatedResponse> fetchDataAsync(List<String> apiUrls) {
long startTime = [Link]();

List<CompletableFuture<String>> futures = [Link]()


.map(this::fetchSingleApiAsync)
.collect([Link]());

return [Link]([Link](new CompletableFuture[0]))


.thenApply(v -> {
List<String> responses = [Link]()
.map(CompletableFuture::join)
.collect([Link]());

long duration = [Link]() - startTime;


return new AggregatedResponse(responses, duration);
});
}

private CompletableFuture<String> fetchSingleApiAsync(String url) {


return [Link](() -> {
try {
return [Link](url, [Link]);
} catch (Exception e) {
return "Error: " + [Link]();
}
});
}

26
// Virtual threads approach (Java 21+)
public AggregatedResponse fetchDataVirtualThreads(List<String> apiUrls) {
long startTime = [Link]();
List<String> responses = new ArrayList<>();

try (var executor = [Link]()) {


List<Future<String>> futures = [Link]()
.map(url -> [Link](() -> {
try {
return [Link](url, [Link]);
} catch (Exception e) {
return "Error: " + [Link]();
}
}))
.collect([Link]());

for (Future<String> future : futures) {


try {
[Link]([Link](5, [Link]));
} catch (Exception e) {
[Link]("Timeout or error");
}
}
}

long duration = [Link]() - startTime;


return new AggregatedResponse(responses, duration);
}
}

public class AggregatedResponse {


private List<String> responses;
private long durationMs;

public AggregatedResponse(List<String> responses, long durationMs) {


[Link] = responses;
[Link] = durationMs;
}

// getters and toString


public List<String> getResponses() { return responses; }
public long getDurationMs() { return durationMs; }

@Override
public String toString() {

27
return [Link]("AggregatedResponse{responses=%d, duration=%dms}",
[Link](), durationMs);
}
}

Project 2: Background Job Processing System


@Component
public class JobProcessingSystem {

private final BlockingQueue<Job> jobQueue = new LinkedBlockingQueue<>();


private final ExecutorService jobProcessor = [Link](3);
private final ScheduledExecutorService scheduler = [Link](1);

@PostConstruct
public void startProcessing() {
// Start job processors
for (int i = 0; i < 3; i++) {
[Link](this::processJobs);
}

// Schedule cleanup task


[Link](this::cleanupCompletedJobs, 0, 30, [Link]);
}

public void submitJob(Job job) {


[Link]([Link]);
[Link]([Link]());
[Link](job);
[Link]("Job {} queued", [Link]());
}

private void processJobs() {


while (![Link]().isInterrupted()) {
try {
Job job = [Link](); // Blocking operation
processJob(job);
} catch (InterruptedException e) {
[Link]().interrupt();
break;
}
}
}

private void processJob(Job job) {


try {

28
[Link]([Link]);
[Link]([Link]());

[Link]("Processing job {} on thread {}",


[Link](), [Link]().getName());

// Simulate job processing


[Link]([Link]());

[Link]([Link]);
[Link]([Link]());

[Link]("Job {} completed successfully", [Link]());

} catch (InterruptedException e) {
[Link]([Link]);
[Link]().interrupt();
} catch (Exception e) {
[Link]([Link]);
[Link]("Job {} failed", [Link](), e);
}
}

private void cleanupCompletedJobs() {


// Implementation for cleanup logic
[Link]("Cleaning up completed jobs...");
}

@PreDestroy
public void shutdown() {
[Link]();
[Link]();
try {
if (![Link](10, [Link])) {
[Link]();
}
if (![Link](5, [Link])) {
[Link]();
}
} catch (InterruptedException e) {
[Link]();
[Link]();
[Link]().interrupt();
}
}
}

29
// Job class
public class Job {
private String id;
private JobStatus status;
private long submittedAt;
private long startedAt;
private long completedAt;
private int durationMs;

public Job(String id, int durationMs) {


[Link] = id;
[Link] = durationMs;
[Link] = [Link];
}

// getters and setters


public String getId() { return id; }
public JobStatus getStatus() { return status; }
public void setStatus(JobStatus status) { [Link] = status; }
public long getSubmittedAt() { return submittedAt; }
public void setSubmittedAt(long submittedAt) { [Link] = submittedAt; }
public long getStartedAt() { return startedAt; }
public void setStartedAt(long startedAt) { [Link] = startedAt; }
public long getCompletedAt() { return completedAt; }
public void setCompletedAt(long completedAt) { [Link] = completedAt; }
public int getDurationMs() { return durationMs; }
}

enum JobStatus {
CREATED, QUEUED, PROCESSING, COMPLETED, FAILED
}

Project 3: Parallel Data Processing Pipeline


@Service
public class DataProcessingPipeline {

private final ExecutorService ioExecutor = [Link](5);


private final ExecutorService cpuExecutor = [Link](
[Link]().availableProcessors());

public CompletableFuture<ProcessingResult> processLargeDataset(String filePath) {


return CompletableFuture
// Stage 1: Read data (I/O intensive)
.supplyAsync(() -> readDataFromFile(filePath), ioExecutor)

30
// Stage 2: Parse data (CPU intensive)
.thenComposeAsync(rawData -> parseData(rawData), cpuExecutor)
// Stage 3: Process in parallel
.thenComposeAsync(this::processDataInParallel, cpuExecutor)
// Stage 4: Aggregate results
.thenApply(this::aggregateResults)
// Stage 5: Save results (I/O intensive)
.thenComposeAsync(result -> saveResults(result), ioExecutor)
.exceptionally(throwable -> {
[Link]("Pipeline failed", throwable);
return new ProcessingResult("Failed", 0, [Link]());
});
}

private String readDataFromFile(String filePath) {


try {
[Link]("Reading data from {} on thread {}", filePath, [Link](
[Link](2000); // Simulate file I/O
return "raw_data_content_from_" + filePath;
} catch (InterruptedException e) {
[Link]().interrupt();
throw new RuntimeException(e);
}
}

private CompletableFuture<List<DataItem>> parseData(String rawData) {


return [Link](() -> {
try {
[Link]("Parsing data on thread {}", [Link]().getName());
[Link](1000); // Simulate parsing

List<DataItem> items = new ArrayList<>();


for (int i = 1; i <= 100; i++) {
[Link](new DataItem("item_" + i, i * 10));
}
return items;
} catch (InterruptedException e) {
[Link]().interrupt();
throw new RuntimeException(e);
}
}, cpuExecutor);
}

private CompletableFuture<List<ProcessedItem>> processDataInParallel(List<DataItem> item


// Split data into chunks for parallel processing
int chunkSize = 25;

31
List<List<DataItem>> chunks = partition(items, chunkSize);

List<CompletableFuture<List<ProcessedItem>>> chunkFutures = [Link]()


.map(chunk -> [Link](() -> processChunk(chunk), cpuExecut
.collect([Link]());

return [Link]([Link](new CompletableFuture[0]))


.thenApply(v -> [Link]()
.map(CompletableFuture::join)
.flatMap(List::stream)
.collect([Link]()));
}

private List<ProcessedItem> processChunk(List<DataItem> chunk) {


[Link]("Processing chunk of {} items on thread {}",
[Link](), [Link]().getName());

return [Link]()
.map(item -> {
try {
[Link](50); // Simulate processing
return new ProcessedItem([Link](), [Link]() * 2, "processed
} catch (InterruptedException e) {
[Link]().interrupt();
throw new RuntimeException(e);
}
})
.collect([Link]());
}

private ProcessingResult aggregateResults(List<ProcessedItem> items) {


[Link]("Aggregating {} results on thread {}",
[Link](), [Link]().getName());

int totalValue = [Link]()


.mapToInt(ProcessedItem::getValue)
.sum();

return new ProcessingResult("Success", totalValue, null);


}

private CompletableFuture<ProcessingResult> saveResults(ProcessingResult result) {


return [Link](() -> {
try {
[Link]("Saving results on thread {}", [Link]().getName())
[Link](1000); // Simulate database save

32
[Link](true);
return result;
} catch (InterruptedException e) {
[Link]().interrupt();
throw new RuntimeException(e);
}
}, ioExecutor);
}

private <T> List<List<T>> partition(List<T> list, int chunkSize) {


List<List<T>> partitions = new ArrayList<>();
for (int i = 0; i < [Link](); i += chunkSize) {
[Link]([Link](i, [Link](i + chunkSize, [Link]())));
}
return partitions;
}

@PreDestroy
public void cleanup() {
[Link]();
[Link]();
}
}

// Data classes
class DataItem {
private String name;
private int value;

public DataItem(String name, int value) {


[Link] = name;
[Link] = value;
}

public String getName() { return name; }


public int getValue() { return value; }
}

class ProcessedItem {
private String name;
private int value;
private String status;

public ProcessedItem(String name, int value, String status) {


[Link] = name;
[Link] = value;

33
[Link] = status;
}

public String getName() { return name; }


public int getValue() { return value; }
public String getStatus() { return status; }
}

class ProcessingResult {
private String status;
private int totalValue;
private String errorMessage;
private boolean saved = false;

public ProcessingResult(String status, int totalValue, String errorMessage) {


[Link] = status;
[Link] = totalValue;
[Link] = errorMessage;
}

public String getStatus() { return status; }


public int getTotalValue() { return totalValue; }
public String getErrorMessage() { return errorMessage; }
public boolean isSaved() { return saved; }
public void setSaved(boolean saved) { [Link] = saved; }
}

Project 4: Real-Time Notification System


@Service
public class NotificationService {

private final Map<String, List<NotificationListener>> subscribers = new ConcurrentHashMa


private final ExecutorService notificationExecutor = [Link](10);

@Async("notificationExecutor")
public CompletableFuture<Void> sendNotification(Notification notification) {
List<NotificationListener> listeners = [Link]([Link]());

if (listeners == null || [Link]()) {


return [Link](null);
}

// Send to all listeners in parallel


List<CompletableFuture<Void>> sendTasks = [Link]()
.map(listener -> sendToListener(listener, notification))

34
.collect([Link]());

return [Link]([Link](new CompletableFuture[0]));


}

private CompletableFuture<Void> sendToListener(NotificationListener listener, Notificati


return [Link](() -> {
try {
[Link]("Sending notification {} to listener {} on thread {}",
[Link](), [Link](), [Link]().getName()

[Link](notification);

// Simulate network delay


[Link](200);

[Link]("Notification {} sent successfully to {}",


[Link](), [Link]());

} catch (Exception e) {
[Link]("Failed to send notification {} to listener {}",
[Link](), [Link](), e);
}
}, notificationExecutor);
}

public void subscribe(String topic, NotificationListener listener) {


[Link](topic, k -> new CopyOnWriteArrayList<>()).add(listener);
[Link]("Listener {} subscribed to topic {}", [Link](), topic);
}

public void unsubscribe(String topic, NotificationListener listener) {


List<NotificationListener> listeners = [Link](topic);
if (listeners != null) {
[Link](listener);
[Link]("Listener {} unsubscribed from topic {}", [Link](), topic);
}
}

@PreDestroy
public void shutdown() {
[Link]();
try {
if (![Link](10, [Link])) {
[Link]();
}

35
} catch (InterruptedException e) {
[Link]();
[Link]().interrupt();
}
}
}

// Supporting classes
class Notification {
private String id;
private String topic;
private String message;
private long timestamp;

public Notification(String id, String topic, String message) {


[Link] = id;
[Link] = topic;
[Link] = message;
[Link] = [Link]();
}

// getters
public String getId() { return id; }
public String getTopic() { return topic; }
public String getMessage() { return message; }
public long getTimestamp() { return timestamp; }
}

interface NotificationListener {
String getId();
void onNotification(Notification notification);
}

@Component
class EmailNotificationListener implements NotificationListener {
@Override
public String getId() {
return "email-listener";
}

@Override
public void onNotification(Notification notification) {
[Link]("Sending email for notification: {}", [Link]());
// Email sending logic
}
}

36
Interview Questions & Answers
Q1: What’s the difference between submit() and execute() in Execu-
torService?
Answer: - execute(Runnable): Fire-and-forget, no return value, exceptions
are handled by UncaughtExceptionHandler - submit(Callable/Runnable):
Returns Future, exceptions can be retrieved via [Link]()
// execute() - no return value
[Link](() -> [Link]("Fire and forget"));

// submit() - returns Future


Future<String> future = [Link](() -> "I can return values");

Q2: Why might [Link]() be dangerous in produc-


tion?
Answer: get() blocks the calling thread indefinitely. In web applications, this
can exhaust the request-handling thread pool.
// BAD - blocks indefinitely
String result = [Link]();

// GOOD - with timeout


String result = [Link](5, [Link]);

// BETTER - non-blocking
[Link](result -> processResult(result));

Q3: What happens if you call an @Async method from the same
class?
Answer: The method executes synchronously because Spring’s AOP proxy is
bypassed (self-invocation problem).
@Service
public class AsyncService {

@Async
public void asyncMethod() {
// This will be async when called from outside
}

public void callerMethod() {


asyncMethod(); // This will be SYNCHRONOUS!

37
}
}

// Solution: Inject self or use separate service


@Service
public class AsyncService {

@Autowired
private AsyncService self; // Self-injection

public void callerMethod() {


[Link](); // Now this will be async
}
}

Q4: How do Virtual Threads differ from Platform Threads?


Answer: - Platform Threads: 1:1 mapping to OS threads, ~1MB stack,
expensive creation - Virtual Threads: M:N mapping, ~KB memory, very
cheap creation
// Platform threads - limited scalability
try (var executor = [Link](1000)) {
// Can handle ~1000 concurrent operations
}

// Virtual threads - massive scalability


try (var executor = [Link]()) {
// Can handle millions of concurrent operations
}

Q5: What are the thread safety issues with CompletableFuture?


Answer: - The CompletableFuture itself is thread-safe - But the tasks you run
and shared state they access may not be - Race conditions can occur with shared
mutable objects
// Thread-safe approach
private final AtomicInteger counter = new AtomicInteger(0);

CompletableFuture<Integer> future = [Link](() -> {


return [Link](); // Atomic operation
});

Q6: How do you handle exceptions in different threading approaches?


Answer:

38
// Traditional Future
try {
String result = [Link]();
} catch (ExecutionException e) {
Throwable cause = [Link](); // Original exception
}

// CompletableFuture
CompletableFuture<String> future = CompletableFuture
.supplyAsync(this::riskyOperation)
.exceptionally(throwable -> "Default value")
.handle((result, throwable) -> {
if (throwable != null) {
[Link]("Operation failed", throwable);
return "Error occurred";
}
return result;
});

// Spring @Async - implement AsyncUncaughtExceptionHandler


@Override
public void handleUncaughtException(Throwable throwable, Method method, Object... params) {
[Link]("Async method {} failed", [Link](), throwable);
}

Q7: What’s the “blocking vs non-blocking” concept?


Answer: - Blocking: Thread waits for operation to complete (e.g.,
[Link]()) - Non-blocking: Thread continues execution, handles result
via callbacks
// Blocking approach
String result = [Link](); // Thread waits here
processResult(result);

// Non-blocking approach
[Link](result -> processResult(result)); // Thread continues immediately

Q8: How do you choose thread pool sizes?


Answer: - CPU-intensive: Number of cores ([Link]().availableProcessors())
- I/O-intensive: Much higher (cores × 2 to cores × 50) - Mixed workload:
Separate pools for different task types
// CPU-intensive pool
int cpuThreads = [Link]().availableProcessors();
ExecutorService cpuPool = [Link](cpuThreads);

39
// I/O-intensive pool
int ioThreads = cpuThreads * 10; // Higher multiplier for I/O
ExecutorService ioPool = [Link](ioThreads);

Best Practices Summary


General Threading Best Practices
1. Choose the right tool:
• Simple sequential: Single-threading
• CPU-intensive: Multi-threading with core-count pools
• I/O-intensive: CompletableFuture or Virtual Threads
• Spring apps: @Async with proper configuration
2. Resource Management:
• Always shutdown executors
• Use try-with-resources when possible
• Monitor thread pool metrics
• Set appropriate timeouts
3. Exception Handling:
• Never ignore InterruptedException
• Use proper exception handlers for async operations
• Log errors with context
• Provide fallback mechanisms
4. Thread Safety:
• Avoid shared mutable state
• Use concurrent collections
• Understand happens-before relationships
• Use atomic operations when appropriate
5. Performance Optimization:
• Profile before optimizing
• Separate thread pools for different workloads
• Use appropriate queue sizes
• Monitor for thread starvation

Spring-Specific Best Practices


1. Configuration:
• Configure multiple executors for different purposes
• Set appropriate pool sizes based on workload
• Use meaningful thread name prefixes
• Configure rejection policies
2. Error Handling:
• Implement AsyncUncaughtExceptionHandler
• Return CompletableFuture for better error propagation

40
• Use @Retryable for transient failures

41

Common questions

Powered by AI

A CustomThreadFactory allows developers to customize how new threads are created and configured within a thread pool. Implementing a CustomThreadFactory provides flexibility in setting thread properties such as names, priorities, and whether they are daemon threads. This can be particularly useful for monitoring, debugging, and managing thread lifecycle, as custom names improve log readability and assist with identifying thread-related issues. Customization can also help with aligning thread configurations to specific application needs, such as ensuring certain threads remain non-daemon for long-running processes .

CompletableFuture offers enhanced functional programming capabilities through method chaining, composition, and improved exception handling. It supports complex asynchronous workflows with dependencies between tasks and provides better error handling compared to raw Future. By implementing both Future and CompletionStage interfaces, it enables the composition of asynchronous operations in a more readable and functional style .

Traditional threads in Java consume a significant amount of memory and can lead to increased overhead due to the cost of managing numerous OS-level threads. They are suited to tasks where the number of concurrent operations is relatively limited. Conversely, virtual threads in Java are lightweight and managed by the JVM, allowing for the execution of millions of concurrent operations with a much smaller memory footprint. This distinction in performance and resource consumption makes virtual threads more efficient for high-concurrency, I/O-bound applications, while traditional threads may be preferred for CPU-intensive tasks where fewer threads are required .

Best practices for handling exceptions in CompletableFuture include using methods like exceptionally() or handle() to manage errors without blocking the main application flow and to provide fallback logic. Handling exceptions this way is crucial for maintaining robust and fault-tolerant asynchronous systems, allowing the application to recover gracefully from failures and continue processing under exceptional conditions. These practices ensure that exception handling is seamlessly integrated into the functional programming model, contributing to the readability and maintainability of the code .

Structured concurrency using virtual threads simplifies concurrency management by organizing concurrent operations within defined scopes that logically group tasks. This method involves creating and managing virtual threads within a specific context, allowing for automatic clean-up and better error handling. With virtual threads, operations are suspended and resumed by the JVM, minimizing complexity and reducing memory overhead. Structured concurrency improves the reliability and maintainability of concurrent systems, offering a clear, organized, and systematic approach to managing the lifecycle and execution of concurrent tasks .

ExecutorService manages thread lifecycles by automatically handling the creation, scheduling, and termination of threads. It uses a pool of worker threads and blocking queues for task submission, which reduces the overhead associated with creating and managing threads explicitly. This approach provides resource control to limit the number of concurrent threads, supports task queuing when threads are busy, and allows for graceful shutdown of threads, enhancing performance and reliability in concurrent applications .

Java concurrency has evolved significantly from Threads, which required manual lifecycle management, to Virtual Threads, which offer enhanced concurrency with less complexity. Initial improvements with ExecutorService provided thread pooling and lifecycle management to enable resource control and scalability for concurrent applications. CompletableFuture introduced functional programming and improved error handling. Virtual Threads represent a significant shift towards lightweight concurrency, enabling applications to handle millions of tasks with minimal memory usage and efficient task suspension and resumption. This evolution reflects a shift in application development towards more scalable and efficient models suitable for modern, high-concurrency, I/O-intensive environments .

ExecutorService helps avoid resource wastage in multithreaded applications by managing a pool of worker threads that can be reused for multiple tasks. This prevents the frequent creation and destruction of threads, which can be resource-intensive. ExecutorService uses blocking queues for task submission and provides efficient lifecycle management for the threads, which reduces overhead and ensures better resource allocation. By controlling thread usage and enabling thread pooling, ExecutorService optimizes CPU and memory utilization, thus improving overall application performance .

Virtual threads are highly beneficial in high-concurrency applications that require handling millions of concurrent operations, such as I/O-intensive workloads and microservices involving numerous external API calls. They offer a lightweight threading mechanism managed by the JVM instead of the OS, allowing for significant concurrency with a minimal memory footprint. Their ability to suspend and resume efficiently makes them ideal for web servers processing many concurrent requests, facilitating massive parallel processing with simpler programming models .

Method chaining in CompletableFuture allows sequencing multiple operations to be executed asynchronously in a streamlined manner, which improves code readability and manages complex workflows. By chaining methods, developers can specify ordered sequences of transformations and actions on the result of asynchronous operations using methods like thenApply(), thenCombine(), and thenCompose(). This approach facilitates building sophisticated and responsive async applications by composing asynchronous tasks like a chain of dependent operations, making the logic easier to understand and maintain .

You might also like