Multithreading & Concurrency
java multithreading concurrency threads Runnable Callable synchronized volatile ReentrantLock ExecutorService CompletableFuture ConcurrentHashMap
Introduction
Modern applications rarely execute only one task at a time.
Consider a banking application.
While one user is:
- Transferring money
Another user is:
- Checking account balance
At the same time, the system is:
- Sending email notifications
- Processing transactions
- Writing audit logs
- Generating reports
- Communicating with payment gateways
If these tasks execute sequentially, the application becomes slow and unresponsive.
Java solves this problem using Multithreading and Concurrency.
Multithreading allows multiple threads to execute simultaneously (or concurrently, depending on available CPU cores), improving responsiveness, throughput, and resource utilization.
Multithreading is heavily used in:
- Spring Boot Applications
- Microservices
- Web Servers (Tomcat, Jetty)
- Banking Systems
- E-Commerce Platforms
- Kafka Consumers
- REST APIs
- Android Applications
- High-Frequency Trading Systems
It is one of the most important advanced Java topics and is frequently asked in senior-level interviews.
In this lesson, you'll learn:
- What is a Process?
- What is a Thread?
- Multithreading
- Thread Lifecycle
- Creating Threads
- Runnable Interface
- Callable Interface
- Thread Synchronization
- Race Conditions
- synchronized Keyword
- volatile Keyword
- Locks (ReentrantLock)
- Atomic Classes
- Executor Framework
- Thread Pools
- CompletableFuture
- Fork/Join Framework
- Concurrent Collections
- Best Practices
- Common Mistakes
- Hands-on Exercises
- Professional Interview Questions
What is a Process?
A Process is an independent program running in memory.
Examples:
- Google Chrome
- IntelliJ IDEA
- Spotify
- Microsoft Word
Each process has:
- Own memory
- Own resources
- Own execution environment
What is a Thread?
A Thread is the smallest unit of execution inside a process.
Example:
Chrome Process├── UI Thread├── Network Thread├── Rendering Thread└── Download Thread
All threads inside a process share:
- Heap Memory
- Open Files
- Network Connections
But each thread has its own:
- Stack
- Program Counter
- Local Variables
Process vs Thread
| Process | Thread |
|---|---|
| Independent execution | Executes within a process |
| Separate memory | Shared heap memory |
| Heavyweight | Lightweight |
| Slower creation | Faster creation |
| Communication is expensive | Communication is easier |
What is Multithreading?
Multithreading is the ability to execute multiple threads concurrently.
Example:
Main Thread↓Thread 1 → Download FileThread 2 → Read DatabaseThread 3 → Send EmailThread 4 → Generate PDF
Benefits:
- Better CPU utilization
- Faster execution
- Improved responsiveness
- Better scalability
Thread Lifecycle
New↓Runnable↓Running↓Blocked / Waiting↓Runnable↓Terminated
States:
- NEW
- RUNNABLE
- BLOCKED
- WAITING
- TIMED_WAITING
- TERMINATED
Creating a Thread
Extending the Thread Class
class MyThread extends Thread {@Overridepublic void run() {System.out.println("Thread Running");}}public class Main {public static void main(String[] args) {MyThread thread =new MyThread();thread.start();}}
Output:
Thread Running
Always call
start(), notrun(). Callingrun()directly executes the method on the current thread.
Creating Threads using Runnable
Preferred approach:
class Task implements Runnable {@Overridepublic void run() {System.out.println("Runnable Executed");}}public class Main {public static void main(String[] args) {Thread thread =new Thread(new Task());thread.start();}}
Advantages:
- Better design
- Supports inheritance from another class
- Encourages separation of task and thread
Runnable using Lambda
Thread thread = new Thread(() ->System.out.println("Lambda Thread"));thread.start();
This is the most common modern style.
Callable Interface
Runnable cannot return values.
Callable can.
Callable<Integer> task = () -> {return 100;};
Used with:
- ExecutorService
- Future
Future
ExecutorService executor =Executors.newSingleThreadExecutor();Future<Integer> future =executor.submit(() -> 500);System.out.println(future.get());executor.shutdown();
Output:
500
Race Condition
Suppose two threads update the same variable.
count++;
Thread A:
Reads 10Writes 11
Thread B:
Reads 10Writes 11
Expected:
12
Actual:
11
This is called a Race Condition.
Synchronization
Synchronization ensures that only one thread enters a critical section at a time.
public synchronized void increment(){count++;}
This prevents concurrent modification of shared state.
Synchronized Block
synchronized(this){count++;}
Synchronizing only the critical section reduces contention.
volatile Keyword
volatile ensures visibility of updates across threads.
private volatile boolean running = true;
Use volatile when:
- Multiple threads read/write a variable
- Atomicity is not required
- Visibility is required
volatile does not make compound operations such as count++ atomic.
ReentrantLock
Provides advanced locking features beyond synchronized.
Lock lock = new ReentrantLock();lock.lock();try{count++;}finally{lock.unlock();}
Advantages:
- Try Lock
- Timed Lock
- Interruptible Lock
- Fair Locking
Atomic Classes
Package:
java.util.concurrent.atomic
Example:
AtomicInteger counter =new AtomicInteger();counter.incrementAndGet();System.out.println(counter.get());
Atomic classes perform thread-safe operations without explicit synchronization for many common use cases.
Executor Framework
Instead of manually creating threads:
ExecutorService executor =Executors.newFixedThreadPool(4);executor.submit(() ->System.out.println("Task"));executor.shutdown();
Benefits:
- Thread Reuse
- Better Performance
- Resource Management
- Scalable Design
Thread Pool
Task Queue↓Thread Pool↓Worker Threads
Instead of creating a new thread for every request, worker threads are reused.
This is how most web servers operate.
CompletableFuture
Introduced in Java 8 for asynchronous programming.
CompletableFuture.supplyAsync(() -> "Java").thenApply(String::toUpperCase).thenAccept(System.out::println);
Output:
JAVA
Benefits:
- Asynchronous execution
- Chaining
- Exception handling
- Non-blocking programming
Fork/Join Framework
Designed for divide-and-conquer algorithms.
Large Task↓Split↓Subtasks↓Parallel Processing↓Merge Results
Used for CPU-intensive workloads.
Concurrent Collections
Package:
java.util.concurrent
Common classes:
- ConcurrentHashMap
- CopyOnWriteArrayList
- BlockingQueue
- ConcurrentLinkedQueue
These collections provide thread-safe access with better scalability than legacy synchronized collections.
Thread Safety
Thread-safe classes prevent inconsistent results when accessed by multiple threads simultaneously.
Examples:
- ConcurrentHashMap
- AtomicInteger
- BlockingQueue
Non-thread-safe:
- ArrayList
- HashMap
- LinkedList
Real-World Example: Banking Transaction
public synchronized void withdraw(double amount){if(balance >= amount){balance -= amount;}}
Synchronization prevents two users from withdrawing the same balance simultaneously.
Real-World Example: Spring Boot
ExecutorService executor =Executors.newFixedThreadPool(10);executor.submit(() ->sendEmail());
Common use cases:
- Email notifications
- PDF generation
- Report generation
- Background jobs
Best Practices
Prefer ExecutorService
Avoid manually creating threads for production applications.
Minimize Synchronization
Synchronize only critical sections to reduce contention.
Prefer Atomic Classes
Use AtomicInteger, AtomicLong, and similar classes for simple counters.
Shutdown Executor Services
Always call:
executor.shutdown();
or shutdownNow() when appropriate.
Avoid Shared Mutable State
Prefer immutable objects or thread-safe data structures whenever possible.
Common Mistakes
Calling run() Instead of start()
run() executes on the current thread.
start() creates a new thread.
Forgetting to Release Locks
Always release ReentrantLock in a finally block.
Overusing Synchronization
Excessive synchronization reduces scalability.
Using HashMap in Concurrent Applications
Use ConcurrentHashMap instead.
Ignoring InterruptedException
Handle interruption properly instead of swallowing it.
Hands-on Exercise
Create a Java program that:
- Creates a thread by extending
Thread. - Creates another thread using
Runnable. - Uses a lambda expression to start a thread.
- Executes tasks using an
ExecutorService. - Returns a value using
CallableandFuture. - Demonstrates a race condition.
- Fixes the race condition using
synchronized. - Uses
AtomicIntegerinstead of synchronization. - Creates an asynchronous pipeline using
CompletableFuture. - Uses a
ConcurrentHashMapin a multi-threaded scenario.
Summary
Java Multithreading enables applications to execute multiple tasks concurrently, improving responsiveness and throughput. Modern Java applications rely heavily on the ExecutorService, CompletableFuture, atomic classes, concurrent collections, and the java.util.concurrent package rather than manually managing threads. Understanding synchronization, thread safety, race conditions, and asynchronous programming is essential for building scalable enterprise systems.
Key Takeaways
- A process can contain multiple threads.
- Threads share heap memory but have separate stacks.
- Prefer implementing
Runnableover extendingThread. - Use
Callablewhen a task must return a result. - Synchronization protects shared mutable state.
volatileprovides visibility but not atomicity.ReentrantLockoffers advanced locking capabilities.- Atomic classes provide efficient thread-safe operations.
- Use
ExecutorServiceinstead of manually creating threads. CompletableFuturesimplifies asynchronous programming.ConcurrentHashMapis preferred overHashtablefor concurrent access.
Professional Interview Questions
1What is the difference between a Process and a Thread?
Professional Answer
A process is an independent program with its own memory space and system resources. A thread is the smallest unit of execution within a process. Threads belonging to the same process share heap memory and resources but maintain separate stacks and program counters. Threads are lightweight and less expensive to create than processes.
Follow-up Questions
- What resources are shared between threads?
- Why are threads considered lightweight?
Interview Tip: Remember: Process → Independent Program, Thread → Lightweight Execution Unit.
2What is the difference between Runnable and Callable?
Professional Answer
Runnable represents a task that does not return a result and cannot throw checked exceptions. Callable represents a task that returns a value and may throw checked exceptions. Callable is executed using an ExecutorService and returns a Future, which can be used to retrieve the computation result.
Follow-up Questions
- Can Runnable return a value?
- Which interface works with Future?
Interview Tip: A quick memory trick: Runnable → No Return, Callable → Returns Result.
3What is the difference between synchronized, volatile, and AtomicInteger?
Professional Answer
synchronized provides mutual exclusion, ensuring that only one thread executes a critical section at a time while also providing visibility guarantees. volatile guarantees that updates to a variable are immediately visible to other threads but does not make compound operations atomic. AtomicInteger performs atomic operations such as incrementing and decrementing without requiring explicit synchronization, making it an efficient choice for thread-safe counters.
Follow-up Questions
- Can volatile prevent race conditions?
- Why is AtomicInteger often faster than using synchronized for counters?
- When would you choose ReentrantLock over synchronized?
Interview Tip: Remember: volatile → Visibility, synchronized → Mutual Exclusion + Visibility, AtomicInteger → Lock-Free Atomic Operations.