Java Multithreading: Complete Guide to Concurrent Programming
Welcome back to the Java Fundamentals series! 👋
What Is a Thread?
A thread is the smallest unit of execution within a process. In Java, every application has at least one thread: the main thread.
A thread is a lightweight subprocess — smallest unit of CPU execution.
Java enables multithreading via:
Thread
classRunnable
interface
Why Use Threads?
- Concurrent execution: Perform multiple tasks simultaneously
- Improved responsiveness: Handle UI + background work
- Utilize multicore processors: Better hardware utilization
Ways to Create Threads
1. Extend Thread
Class
class MyThread extends Thread {
public void run() {
System.out.println("Thread running");
}
}
2. Implement Runnable
Interface
Preferred in clean designs since it allows multiple inheritance via interfaces.
class MyRunnable implements Runnable {
public void run() {
System.out.println("Thread running");
}
}
3. Implement Callable
(Java 5+)
If you want the thread to return a value and throw checked exceptions.
Callable<Integer> task = () -> { return 10; };
FutureTask<Integer> future = new FutureTask<>(task);
new Thread(future).start();
Thread Lifecycle
States:
NEW → RUNNABLE → RUNNING → TERMINATED
↓
BLOCKED / WAITING / TIMED_WAITING
Common Thread Methods:
Method | Purpose |
---|---|
start() | Starts the thread (calls run() ) |
run() | Actual code to execute |
sleep(ms) | Sleep for given milliseconds |
join() | Waits for another thread to finish |
isAlive() | Check if thread is still running |
Thread Priorities
Values: 1 (MIN_PRIORITY
) to 10 (MAX_PRIORITY
), default is 5.
Thread t = new Thread();
t.setPriority(Thread.MAX_PRIORITY);
Note: Thread priorities are hints, not guarantees. OS thread scheduler decides.
Synchronization
To avoid race conditions when multiple threads access shared data.
synchronized void increment() {
count++;
}
synchronized(this) {
count++;
}
Inter-thread Communication
wait()
, notify()
, notifyAll()
— object methods for coordinating threads.
synchronized(obj) {
obj.wait();
}
synchronized(obj) {
obj.notify();
}
Note: Avoid overusing — better use BlockingQueue
or modern concurrency tools.
Executor Framework (Modern Approach)
From Java 5 onwards — better thread management.
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.execute(() -> System.out.println("Task"));
executor.shutdown();
You can use:
FixedThreadPool
CachedThreadPool
SingleThreadExecutor
ScheduledThreadPool
Why use this?
- Avoids creating threads manually
- Better performance tuning
- Cleaner shutdown
Thread-safe Collections
Vector
(legacy)Collections.synchronizedList(new ArrayList<>())
ConcurrentHashMap
CopyOnWriteArrayList
Why Is Processing Huge Lists a Problem?
When processing a large list (say millions of entries):
- Single-threaded: Slow, as it processes sequentially
- Multithreaded: Can improve throughput by dividing the list into chunks and processing in parallel
But you have challenges:
- Correctly partitioning work
- Avoiding race conditions on shared data
- Not exceeding memory limits (GC overhead)
- Choosing the right concurrency tools
Processing Large Lists with Multithreading
1. Partition the List into Chunks
You can't have multiple threads working on the same list positions unless you properly partition.
Note: Use subList
carefully because it's a view, not a copy — changes in sublist affect original list.
int chunkSize = 1000;
for (int i = 0; i < list.size(); i += chunkSize) {
List<String> subList = list.subList(i, Math.min(i + chunkSize, list.size()));
executor.execute(() -> processSubList(subList));
}
2. Use ExecutorService (FixedThreadPool or ForkJoinPool)
- FixedThreadPool when tasks are similar in size
- ForkJoinPool when task splitting is recursive (divide-and-conquer)
ExecutorService executor = Executors.newFixedThreadPool(10);
// or
ForkJoinPool customPool = new ForkJoinPool(8);
customPool.submit(() -> list.parallelStream().forEach(item -> process(item)));
3. Avoid Shared Mutable State
No shared counters, maps, or lists without protection.
Use:
ConcurrentHashMap
AtomicInteger
/AtomicLong
- Avoid non-thread-safe collections like
ArrayList
unless synchronized
Key Scenarios
Case 1: Read-Only Operations
Use parallelStream()
directly — it's backed by ForkJoinPool
.
list.parallelStream().forEach(item -> process(item));
Caveat: Number of threads is Runtime.getRuntime().availableProcessors()
by default.
You can control it:
ForkJoinPool customPool = new ForkJoinPool(16);
customPool.submit(() -> list.parallelStream().forEach(...));
Case 2: Writing Results Back
If multiple threads write to a shared result, use:
ConcurrentLinkedQueue
ConcurrentHashMap
CopyOnWriteArrayList
(less performant on large writes)- Or synchronize explicitly (costly)
Case 3: High Volume Real-Time Processing
If you continuously receive new items:
- Use
BlockingQueue
- Producer threads put data
- Consumer threads poll and process
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
Then use an executor to consume items from it.
Example: Parallel Processing of List
List<Integer> numbers = IntStream.range(0, 1_000_000)
.boxed()
.collect(Collectors.toList());
ForkJoinPool customThreadPool = new ForkJoinPool(8);
customThreadPool.submit(() ->
numbers.parallelStream().forEach(num -> System.out.println(num))
);
customThreadPool.shutdown();
Multithreading Pitfalls with Lists
ConcurrentModificationException
if modifying list while iterating without proper safeguardsOutOfMemoryError
if too many threads created- Threads competing for GC time on large object graphs
- Shared list's state corruption without synchronization
Golden rule:
- ✅ Immutable data is the safest in multithreading
- ✅ Mutable shared data needs Concurrent collections or explicit locks
Summary: Multithreading Key Rules with Lists
Issue | What to do |
---|---|
Thread safety | Use concurrent collections or sync |
Task division | Partition list into non-overlapping chunks |
Thread management | Use ExecutorService, not raw threads |
Shared results | Use thread-safe collections |
Exception handling | Use Future and check for exceptions |
Resource limits | Keep thread count ~ CPU cores |
Advanced Patterns
Producer-Consumer Pattern with BlockingQueue
When to use:
- You have one or more producers generating tasks (e.g., reading large list data)
- And one or more consumers processing those tasks concurrently
How it helps:
- Decouples task production and consumption
- Handles backpressure — consumers block when no data, producers block when queue full
BlockingQueue<String> queue = new LinkedBlockingQueue<>(1000); // bounded
// Producer thread(s)
new Thread(() -> {
for (String item : largeList) {
queue.put(item); // blocks if full
}
queue.put("EOF"); // signal end
}).start();
// Consumer thread(s)
for (int i = 0; i < numConsumers; i++) {
new Thread(() -> {
while (true) {
String item = queue.take(); // blocks if empty
if ("EOF".equals(item)) break;
process(item);
}
}).start();
}
Fork/Join Framework for Divide and Conquer
When to use:
- Recursive decomposition of tasks
- Large lists can be split into smaller tasks that themselves split further
Advantage:
- Work-stealing algorithm balances load efficiently
- Fine-grained parallelism
class ListProcessor extends RecursiveTask<Integer> {
private List<Integer> list;
private int start, end;
private static final int THRESHOLD = 1000;
public ListProcessor(List<Integer> list, int start, int end) {
this.list = list;
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
if (end - start <= THRESHOLD) {
// process directly
int sum= 0;
for (int i= start; i < end; i++) {
sum = list.get(i);
}
return sum;
} else {
int mid= (start + end) / 2;
ListProcessor left= new ListProcessor(list, start, mid);
ListProcessor right= new ListProcessor(list, mid, end);
left.fork();
int rightResult= right.compute();
int leftResult= left.join();
return leftResult + rightResult;
}
}
}
Thread Confinement
What:
- Limit a piece of data to be accessed by only one thread
- Instead of synchronization, each thread has its own copy of data
Application:
- Process different chunks of a list in threads, each with its own local result
- After processing, merge results
Benefit:
- Avoids locks, reduces contention
Futures and CompletableFuture for Asynchronous Processing
When:
- You want to submit tasks and retrieve results asynchronously
- Chain tasks, handle exceptions, combine multiple async results
ExecutorService executor = Executors.newFixedThreadPool(4);
List<Future<Integer>> futures = new ArrayList<>();
for (List<Integer> chunk : chunks) {
Future<Integer> future = executor.submit(() -> process(chunk));
futures.add(future);
}
// Later, gather results
int total = 0;
for (Future<Integer> f : futures) {
total += f.get(); // blocks if result not ready
}
CompletableFuture allows chaining:
CompletableFuture.supplyAsync(() -> processChunk(chunk), executor)
.thenApply(result -> result * 2)
.exceptionally(ex -> { log(ex); return 0; });
Work Stealing
ForkJoinPool
's work-stealing: idle threads steal tasks from busy threads.
- Best for irregular, unbalanced tasks
- Avoid fixed thread pools for such tasks
Handling Exceptions in Multithreaded List Processing
- With
ExecutorService.submit()
, exceptions are captured inFuture
- Use
Future.get()
to detect exceptions; otherwise, they're swallowed - Use
ThreadPoolExecutor
hooks for logging uncaught exceptions - With
CompletableFuture
, useexceptionally()
orhandle()
to recover
Batch Processing with Backpressure
- Avoid flooding thread pools or queues with all tasks at once
- Submit tasks in batches
- Wait for batch completion before submitting next batch
- Prevents memory exhaustion and overload
Parallel Pipelines
- Split list processing into stages (like filters, maps)
- Use thread-safe queues between stages
- Each stage is processed by dedicated thread pools
Summary Table
Pattern | When to Use | Pros | Cons |
---|---|---|---|
Producer-Consumer | Decoupled production/consumption | Backpressure, easy coordination | Complex shutdown |
Fork/Join | Recursive divide and conquer | Work stealing, load balancing | Harder debugging |
Thread Confinement | Avoid shared mutable state | No locking, better performance | Needs merge logic |
Futures/CompletableFuture | Async, chaining tasks | Flexible async processing | Complexity for beginners |
Batch Processing | Limit resource usage | Memory control, stability | Increased latency |
Parallel Pipelines | Multiple processing steps | Decoupled, scalable | Complexity |
Conclusion
- Thread management is crucial for performance and stability
- Executor framework provides better control than raw threads
- Thread-safe collections prevent data corruption
- Proper partitioning is key to effective parallel processing
- Exception handling becomes more complex in multithreaded environments
- Choose the right pattern based on your specific use case
Happy coding! 💻