Part 1: Fundamentals, Thread Lifecycle, Thread Creation and Synchronization
Introduction
Modern applications often perform multiple tasks simultaneously. A web application may handle user requests, process payments, send emails, update databases, and generate reports at the same time.
Java Multithreading enables concurrent execution of multiple tasks within a single process, improving responsiveness, throughput, and resource utilization.
Common Use Cases
Processing HTTP requests
File uploads and downloads
Background jobs
Email notifications
Real-time chat applications
Video streaming platforms
Banking systems
Event-driven applications
What is a Thread?
A thread is the smallest unit of execution within a process.
Multiple threads can execute concurrently while sharing the same memory space and application resources.
Use Cases
Request processing
Background tasks
File processing
Notifications
Report generation
Process vs Thread
Process
A process is an independent program in execution.
Examples
Chrome Browser
IntelliJ IDEA
MySQL Database
Thread
A thread is a lightweight execution unit inside a process.
Multiple threads share:
Heap Memory
Application Resources
Database Connections
Network Connections
Comparison
| Feature | Process | Thread |
|---|---|---|
| Memory | Separate | Shared |
| Creation Cost | High | Low |
| Resource Usage | High | Low |
| Communication | Expensive | Fast |
| Context Switching | Slow | Fast |
Benefits of Multithreading
Improved Responsiveness
Applications remain responsive while background operations execute.
Example: Users can continue browsing while a file uploads.
Better CPU Utilization
Modern CPUs contain multiple cores.
Multithreading helps utilize available CPU cores efficiently.
Increased Throughput
Multiple requests can be processed simultaneously.
Example: A web server can process hundreds of requests concurrently.
Reduced Waiting Time
Database calls, API calls, and file operations can execute in parallel.
Thread Lifecycle
A thread goes through multiple states during its lifetime.
NEW
↓
RUNNABLE
↓
RUNNING
↓
BLOCKED / WAITING / TIMED_WAITING
↓
TERMINATED
NEW: Thread object created but not started.
RUNNABLE: Ready for execution and waiting for CPU allocation.
RUNNING: Currently executing.
BLOCKED: Waiting to acquire a lock.
WAITING: Waiting indefinitely for another thread.
TIMED_WAITING: Waiting for a specified period.
TERMINATED: Execution completed.
Use Cases
Thread dump analysis
Performance tuning
Production troubleshooting
Concurrency debugging
Creating Threads
Java provides multiple ways to create threads.
1. Extending Thread Class
Creates a thread by extending the Thread class.
Example
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread Running");
}
}
new MyThread().start();
Use Cases
Learning thread basics
Small utility applications
2. Implementing Runnable Interface
Runnable separates task logic from thread creation.
Example
Runnable task =
() -> System.out.println("Runnable Running");
new Thread(task).start();
Advantages
Preferred approach
Better code design
Supports lambda expressions
Allows extending another class
Use Cases
Background jobs
Logging systems
Notification processing
3. Implementing Callable Interface
Callable can return a value and throw checked exceptions.
Example
Callable<String> task =
() -> "Task Completed";
Advantages
Returns results
Supports exception handling
Integrates with Executor Framework
Use Cases
Database operations
API calls
Report generation
Thread Class Methods
start()
Starts a new thread and internally invokes run().
Use Cases
Background processing
Parallel execution
run()
Contains thread execution logic.
Important Notes
Calling
run()directly does not create a new thread.Always use
start()for concurrent execution.
sleep()
Pauses execution for a specified duration.
Example
Thread.sleep(1000);
Use Cases
Rate limiting
Retry mechanisms
Polling systems
join()
Waits for another thread to complete.
Example
Downloader d = new Downloader();
d.start();
d.join();
System.out.println("Processing File");
Use Cases
File processing workflows
Task dependency management
interrupt()
Requests interruption of a running thread.
Example
task.start();
task.interrupt();
Use Cases
Graceful shutdown
Task cancellation
isAlive()
Checks whether a thread is still running.
Use Cases
Monitoring task completion
Health checks
yield()
Suggests the scheduler switch execution to another thread.
Use Cases
Thread scheduling experiments
Concurrency testing
Thread Safety
Thread Safety ensures shared resources behave correctly when accessed by multiple threads simultaneously.
Non Thread-Safe Example
public class Counter {
private int count = 0;
public int increment() {
return ++count;
}
}
Problem
Multiple threads may update count simultaneously.
Results become inconsistent.
This issue is called a Race Condition.
Example
Expected Result:
10000
Actual Result:
8734
Use Cases
Banking systems
Inventory management
Order processing
Payment systems
Synchronization
Synchronization controls access to shared resources and prevents data inconsistency.
synchronized Keyword
Allows only one thread at a time to execute a synchronized block or method.
Example
public synchronized int increment() {
return ++count;
}
Advantages
Easy to implement
Prevents race conditions
Built into Java
Use Cases
Shared counters
Account balance updates
Inventory management
Atomic Classes
Atomic classes provide lock-free and thread-safe operations on variables.
Common Atomic Classes
AtomicInteger
AtomicLong
AtomicBoolean
AtomicReference
Example
private AtomicInteger counter =
new AtomicInteger();
public int increment() {
return counter.incrementAndGet();
}
Use Cases
Request counters
Statistics collection
Metrics monitoring
Sequence generation
Advantages
Thread-safe
Non-blocking
Better performance than synchronized
volatile Keyword
The volatile keyword ensures updates made by one thread become immediately visible to other threads.
Example
private volatile boolean running = true;
Use Cases
Application shutdown flags
Status indicators
Feature toggles
Important Notes
Provides visibility.
Does not provide atomicity.
Does not replace synchronization.
ReentrantLock
ReentrantLock provides advanced locking capabilities compared to synchronized.
Example
private final ReentrantLock lock =
new ReentrantLock();
lock.lock();
try {
// business logic
} finally {
lock.unlock();
}
Advantages
Fair locking support
Try-lock functionality
Interruptible locking
Better control over locking behavior
Use Cases
Resource management
High-concurrency applications
Complex locking scenarios
ReadWriteLock
ReadWriteLock allows multiple readers but only one writer at a time.
Example
ReadWriteLock lock =
new ReentrantReadWriteLock();
Advantages
Better performance for read-heavy workloads
Reduced lock contention
Use Cases
Cache systems
Configuration management
Read-heavy applications
Thread Communication
wait()
Causes the current thread to wait until notified.
Example
synchronized(lock) {
lock.wait();
}
Use Cases
Producer Consumer Pattern
Task Queues
notify()
Wakes up one waiting thread.
Example
synchronized(lock) {
lock.notify();
}
Use Cases
Resume blocked consumer
Signal task completion
notifyAll()
Wakes up all waiting threads.
Example
synchronized(lock) {
lock.notifyAll();
}
Use Cases
Shared resource availability
Broadcast notifications
Common Concurrency Problems
Race Condition
Multiple threads modify shared data simultaneously causing inconsistent results.
Deadlock
Two or more threads wait indefinitely for each other.
Starvation
A thread never gets sufficient CPU resources because other threads continuously get priority.
Livelock
Threads remain active but continuously react to each other without making progress.
Real-World Applications of Multithreading
Web Servers
Spring Boot
Tomcat
Jetty
Banking Systems
Transaction processing
Audit logging
Notification services
File Processing Systems
Parallel file uploads
Data transformation
Report generation
Messaging Systems
Kafka consumers
RabbitMQ consumers
Event processing
Streaming Applications
Video processing
Audio processing
Live streaming
Part 2: Executor Framework, Thread Pools, Future, CompletableFuture and Concurrent Collections
Why Not Create Threads Manually?
Creating threads using new Thread() works for small applications but becomes difficult to manage in enterprise systems.
Problems
High thread creation cost
Increased memory usage
Difficult lifecycle management
Poor scalability
No thread reuse
Example
new Thread(() -> processOrder()).start();
new Thread(() -> sendEmail()).start();
new Thread(() -> generateInvoice()).start();
Creating thousands of threads this way can impact performance.
The Executor Framework solves these problems.
Executor Framework
The Executor Framework provides a higher-level API for managing and executing asynchronous tasks.
Instead of creating threads manually, tasks are submitted to a thread pool.
Benefits
Thread reuse
Better resource management
Improved performance
Easier scalability
Centralized task execution
Architecture
Task
↓
Executor
↓
ExecutorService
↓
Thread Pool
↓
Worker Threads
Example
ExecutorService executor =
Executors.newFixedThreadPool(5);
executor.submit(() ->
System.out.println("Task Executed"));
executor.shutdown();
Use Cases
REST APIs
Batch processing
Message processing
Background jobs
Notification services
Executor vs ExecutorService
Executor
Basic interface for executing tasks.
Example
Executor executor =
Executors.newSingleThreadExecutor();
executor.execute(() ->
System.out.println("Running"));
ExecutorService
Provides advanced functionality.
Features
Submit tasks
Shutdown thread pools
Return results
Manage task lifecycle
Thread Pools
A Thread Pool is a collection of reusable worker threads.
Benefits
Reduces thread creation overhead
Improves performance
Controls resource usage
Supports high concurrency
Fixed Thread Pool
Creates a fixed number of threads.
Example
ExecutorService executor =
Executors.newFixedThreadPool(5);
Use Cases
REST APIs
Order processing
Payment processing
Microservices
Cached Thread Pool
Creates threads as needed and reuses idle threads.
Example
ExecutorService executor =
Executors.newCachedThreadPool();
Use Cases
Short-lived tasks
Lightweight asynchronous operations
Burst workloads
Single Thread Executor
Uses a single worker thread.
Example
ExecutorService executor =
Executors.newSingleThreadExecutor();
Use Cases
Sequential processing
Logging systems
Event processing
Scheduled Thread Pool
Executes tasks after a delay or periodically.
Example
ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(2);
scheduler.scheduleAtFixedRate(
() -> System.out.println("Running"),
0,
5,
TimeUnit.SECONDS);
Use Cases
Health checks
Cleanup jobs
Scheduled reports
Periodic notifications
ThreadPoolExecutor
ThreadPoolExecutor provides complete control over thread pool behavior.
Example
ThreadPoolExecutor executor =
new ThreadPoolExecutor(
2,
4,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2));
Important Parameters
Core Pool Size
Maximum Pool Size
Keep Alive Time
Work Queue
Use Cases
High-volume APIs
Batch processing
Enterprise applications
Real-World Thread Pool Example
Consider a banking application receiving customer requests.
Example
ExecutorService service =
Executors.newFixedThreadPool(4);
for (int i = 1; i <= 10; i++) {
service.submit(
() -> System.out.println(
Thread.currentThread()
.getName()));
}
Benefits
Reuses threads
Handles multiple requests efficiently
Reduces thread creation overhead
Callable Interface
Callable is similar to Runnable but can return a value and throw checked exceptions.
Example
Callable<String> task =
() -> "Order Processed";
Advantages
Returns values
Supports exceptions
Works with Future
Use Cases
Database queries
API calls
Report generation
Data aggregation
Future
Future represents the result of an asynchronous computation.
Example
ExecutorService executor =
Executors.newSingleThreadExecutor();
Future<String> future =
executor.submit(
() -> "Task Completed");
System.out.println(
future.get());
Common Methods
get()
get(timeout, unit)
cancel()
isDone()
isCancelled()
Example
future.get(
5,
TimeUnit.SECONDS);
Limitations
get() blocks the current thread.
Difficult to combine multiple asynchronous tasks.
CompletableFuture
CompletableFuture simplifies asynchronous and non-blocking programming.
Benefits
Non-blocking execution
Task chaining
Better error handling
Parallel processing support
supplyAsync()
Used when a result is required.
Example
CompletableFuture<String> future =
CompletableFuture.supplyAsync(
() -> "Hello");
Use Cases
API calls
Database queries
Background calculations
runAsync()
Used when no result is required.
Example
CompletableFuture<Void> future =
CompletableFuture.runAsync(
() -> System.out.println(
"Running"));
Use Cases
Logging
Notifications
Background cleanup
thenApply()
Transforms the result of a previous task.
Example
CompletableFuture<String> future =
CompletableFuture
.supplyAsync(() -> "Java")
.thenApply(
value -> value + " 21");
Use Cases
DTO mapping
Data transformation
Response enrichment
thenAccept()
Consumes the result without returning another value.
Example
CompletableFuture
.supplyAsync(() -> "Order")
.thenAccept(System.out::println);
Use Cases
Logging
Notifications
Auditing
thenRun()
Executes another task after completion.
Example
CompletableFuture
.runAsync(() -> processOrder())
.thenRun(() -> sendEmail());
Use Cases
Post-processing actions
Notifications
Workflow completion
thenCompose()
Chains dependent asynchronous tasks.
Example
CompletableFuture<String> future =
CompletableFuture
.supplyAsync(() -> "User")
.thenCompose(
user ->
CompletableFuture
.supplyAsync(
() -> user + " Details"));
Use Cases
User → Orders
Orders → Payments
Workflow processing
thenCombine()
Combines results from independent tasks.
Example
CompletableFuture<String> user =
CompletableFuture.supplyAsync(
() -> "User");
CompletableFuture<String> orders =
CompletableFuture.supplyAsync(
() -> "Orders");
user.thenCombine(
orders,
(u, o) -> u + " " + o);
Use Cases
Dashboard loading
API aggregation
Microservice communication
allOf()
Waits for all tasks to complete.
Example
CompletableFuture.allOf(
future1,
future2,
future3);
Use Cases
Product Details
Inventory
Pricing
Reviews
Load everything in parallel before sending the response.
anyOf()
Returns when any task completes.
Example
CompletableFuture.anyOf(
future1,
future2,
future3);
Use Cases
Multiple service providers
Fastest response wins
Fallback strategies
exceptionally()
Handles exceptions in asynchronous pipelines.
Example
CompletableFuture<String> future =
CompletableFuture
.supplyAsync(() -> {
throw new RuntimeException();
})
.exceptionally(
ex -> "Fallback Value");
Use Cases
Error recovery
Fallback responses
Resilience patterns
Real-World CompletableFuture Example
A Product Details page may require data from multiple services.
Services
Product Service
Inventory Service
Pricing Service
Review Service
Example
CompletableFuture<Product> product =
fetchProduct();
CompletableFuture<Inventory> inventory =
fetchInventory();
CompletableFuture<Price> price =
fetchPrice();
CompletableFuture<Review> review =
fetchReview();
CompletableFuture.allOf(
product,
inventory,
price,
review).join();
Benefits
Faster response times
Better scalability
Improved user experience
Concurrent Collections
Java provides thread-safe collections designed for concurrent access.
Benefits
Thread-safe
Better scalability
Reduced locking overhead
ConcurrentHashMap
Thread-safe alternative to HashMap.
Example
ConcurrentHashMap<String, String> map =
new ConcurrentHashMap<>();
map.put("1", "Java");
Use Cases
Caching
Session storage
Metrics collection
CopyOnWriteArrayList
Creates a new copy of the collection on every write operation.
Example
CopyOnWriteArrayList<String> list =
new CopyOnWriteArrayList<>();
Use Cases
Configuration data
Read-heavy applications
Listener registration
BlockingQueue
Thread-safe queue designed for Producer-Consumer scenarios.
Example
BlockingQueue<String> queue =
new LinkedBlockingQueue<>();
queue.put("Task");
String task = queue.take();
Use Cases
Task scheduling
Message processing
Producer Consumer Pattern
ConcurrentLinkedQueue
Non-blocking thread-safe queue.
Example
ConcurrentLinkedQueue<String> queue =
new ConcurrentLinkedQueue<>();
queue.offer("Task");
Use Cases
Event processing
High-throughput systems
Message queues
CountDownLatch
Allows one or more threads to wait until a set of operations completes.
Example
CountDownLatch latch =
new CountDownLatch(3);
latch.countDown();
latch.await();
Use Cases
Parallel service startup
Batch processing
Integration testing
CyclicBarrier
Allows multiple threads to wait for each other before proceeding.
Example
CyclicBarrier barrier =
new CyclicBarrier(3);
Use Cases
Parallel computations
Simulation systems
Multi-stage processing
Semaphore
Controls access to a limited number of resources.
Example
Semaphore semaphore =
new Semaphore(5);
semaphore.acquire();
try {
// business logic
} finally {
semaphore.release();
}
Use Cases
Database connection pools
Rate limiting
Resource management
Executor Framework Best Practices
Prefer ExecutorService over raw threads.
Always shutdown thread pools.
Use FixedThreadPool for predictable workloads.
Use ScheduledExecutorService for scheduled jobs.
Avoid creating excessive threads.
Use CompletableFuture for async workflows.
Use Concurrent Collections for shared data.
Monitor thread pool utilization in production.
Part 3: Fork/Join Framework, Virtual Threads (Java 21) and Concurrency Best Practices
Evolution of Java Concurrency
Java concurrency has evolved significantly over the years.
Java 1.0
- Thread
- Runnable
Java 5
- Executor Framework
- Callable
- Future
- Concurrent Collections
- Atomic Classes
Java 7
- Fork/Join Framework
Java 8
- CompletableFuture
- Parallel Streams
Java 21
- Virtual Threads
Fork/Join Framework
The Fork/Join Framework was introduced in Java 7 for efficient parallel processing of large tasks.
It follows the Divide and Conquer approach.
Benefits
Efficient parallel processing
Better CPU utilization
Automatic workload balancing
Recursive task execution
Use Cases
Data analytics
Image processing
File scanning
Scientific calculations
Large dataset processing
ForkJoinPool
ForkJoinPool is the core implementation of the Fork/Join Framework.
Example
ForkJoinPool pool =
new ForkJoinPool();
Responsibilities
Manage worker threads
Execute subtasks
Balance workload
Implement work stealing
Use Cases
CPU-intensive workloads
Parallel processing
Large computations
RecursiveTask
RecursiveTask is used when a task returns a result.
Example
class SumTask
extends RecursiveTask<Integer> {
@Override
protected Integer compute() {
// split task
return result;
}
}
Use Cases
Sum calculations
Data aggregation
Report generation
Analytics processing
RecursiveAction
RecursiveAction is used when no result is required.
Example
class FileProcessor
extends RecursiveAction {
@Override
protected void compute() {
// process files
}
}
Use Cases
File processing
Batch updates
Data migration
Background cleanup
Complete Fork/Join Example
Suppose we want to calculate the sum of a large array.
Example
class SumTask extends RecursiveTask<Integer> {
private int[] numbers;
private int start;
private int end;
public SumTask(
int[] numbers,
int start,
int end) {
this.numbers = numbers;
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
if (end - start <= 5) {
int sum = 0;
for (int i = start;
i < end;
i++) {
sum += numbers[i];
}
return sum;
}
int middle =
(start + end) / 2;
SumTask left =
new SumTask(
numbers,
start,
middle);
SumTask right =
new SumTask(
numbers,
middle,
end);
left.fork();
return right.compute()
+ left.join();
}
}
Execution
int[] numbers =
{1,2,3,4,5,6,7,8,9,10};
ForkJoinPool pool =
new ForkJoinPool();
int total =
pool.invoke(
new SumTask(
numbers,
0,
numbers.length));
System.out.println(total);
Benefits
Parallel execution
Better CPU utilization
Automatic task splitting
Work Stealing Algorithm
Work Stealing is the key optimization behind the Fork/Join Framework.
When a worker thread becomes idle, it steals tasks from busy worker threads.
Example
Thread-1 : Busy
Thread-2 : Busy
Thread-3 : Idle
Thread-3 steals work
from Thread-1 or Thread-2
Benefits
Better throughput
Improved CPU utilization
Reduced idle time
Automatic load balancing
Parallel Streams
Parallel Streams internally use ForkJoinPool for parallel execution.
Example
List<Integer> numbers =
Arrays.asList(
1,2,3,4,5);
numbers.parallelStream()
.forEach(
System.out::println);
Use Cases
Data transformation
Analytics
Aggregation
Reporting
Important Notes
Best suited for CPU-intensive tasks.
Avoid for database calls.
Avoid for external API calls.
Virtual Threads (Java 21)
Virtual Threads are lightweight threads managed by the JVM instead of the operating system.
They were introduced as a stable feature in Java 21 under Project Loom.
Benefits
Lightweight
Low memory usage
Massive scalability
Simplified concurrent programming
Use Cases
REST APIs
Microservices
Database operations
Network communication
High-concurrency applications
Traditional Threads vs Virtual Threads
| Feature | Traditional Thread | Virtual Thread |
|---|---|---|
| Managed By | OS | JVM |
| Creation Cost | High | Very Low |
| Memory Usage | High | Low |
| Scalability | Thousands | Millions |
| Blocking Calls | Expensive | Cheap |
| Context Switching | Expensive | Lightweight |
Creating Virtual Threads
Example
Thread.startVirtualThread(
() -> {
System.out.println(
Thread.currentThread());
});
Alternative Approach
Thread thread =
Thread.ofVirtual()
.start(() ->
System.out.println(
"Running"));
Use Cases
API requests
Database calls
File operations
Message processing
Virtual Thread Executor
Java provides a dedicated ExecutorService for Virtual Threads.
Example
try (ExecutorService executor =
Executors
.newVirtualThreadPerTaskExecutor()) {
executor.submit(
() -> processOrder());
}
Benefits
One virtual thread per task
Massive scalability
Simplified concurrency
Use Cases
Microservices
High-volume APIs
Event-driven systems
Carrier Threads
Virtual Threads do not directly run on OS threads.
Instead, they are scheduled on a smaller number of platform threads called Carrier Threads.
Workflow
Virtual Threads
|
v
Carrier Threads
|
v
Operating System Threads
Benefits
Reduced resource consumption
Better scalability
Efficient scheduling
When to Use Virtual Threads
Recommended For
Database calls
REST API calls
File operations
Network communication
Messaging systems
Example
Request
|
Database Call
|
External API Call
|
Response
Virtual Threads excel in I/O-heavy applications.
When Not to Use Virtual Threads
Avoid For
Video encoding
Image rendering
Scientific computations
Machine learning calculations
CPU-intensive analytics
Better Choice
Fork/Join Framework
Parallel Streams
Executor Framework
Choosing the Right Concurrency Model
| Scenario | Recommended Approach |
|---|---|
| Small Application | Thread / Runnable |
| REST APIs | Executor Framework |
| Database Operations | Virtual Threads |
| API Aggregation | CompletableFuture |
| Scheduled Jobs | ScheduledExecutorService |
| Parallel Computation | Fork/Join Framework |
| High-Concurrency Systems | Virtual Threads |
| Data Processing | Fork/Join Framework |
Common Multithreading Mistakes
Creating excessive threads manually.
Forgetting to shutdown ExecutorService.
Using synchronized everywhere.
Ignoring race conditions.
Blocking CompletableFuture using get().
Sharing mutable objects without synchronization.
Using parallel streams for database calls.
Ignoring InterruptedException.
Multithreading Best Practices
Prefer ExecutorService over raw threads.
Prefer CompletableFuture for async workflows.
Use Concurrent Collections for shared data.
Use Atomic classes for counters.
Use Virtual Threads for I/O-intensive workloads.
Keep shared mutable state minimal.
Release locks in finally blocks.
Monitor thread pool utilization.
Handle exceptions properly.
Avoid unnecessary synchronization.
Common Questions
Runnable vs Callable
Runnable does not return a value.
Callable returns a value and supports exceptions.
synchronized vs ReentrantLock
synchronized is simple and built-in.
ReentrantLock provides advanced locking features.
AtomicInteger vs synchronized
AtomicInteger is non-blocking.
synchronized uses locking.
Future vs CompletableFuture
Future supports basic asynchronous execution.
CompletableFuture supports chaining and composition.
CountDownLatch vs CyclicBarrier
CountDownLatch is one-time use.
CyclicBarrier can be reused.
Fork/Join Framework vs Executor Framework
Fork/Join is designed for recursive parallel computation.
Executor Framework is designed for general task execution.
Traditional Thread vs Virtual Thread
Traditional threads are OS-managed.
Virtual Threads are JVM-managed and highly scalable.
Real-World Architecture Example
Modern microservices often combine multiple concurrency approaches.
Request Flow
Client Request
|
Virtual Thread
|
-----------------------
| | |
User Order Payment
Service Service Service
| | |
-----------------------
|
CompletableFuture
|
Response
Technologies Used
Virtual Threads → Handle incoming requests
CompletableFuture → Parallel service calls
Executor Framework → Background jobs
ConcurrentHashMap → Shared caching
Fork/Join Framework → Large dataset processing
Benefits
High scalability
Better responsiveness
Efficient resource utilization
Improved throughput