đ§Ž Parallelism â Exploiting All Cores Like a Pro
Welcome to Pillar 8 of the Concurrency & Multithreading: The Ultimate Engineerâs Bible series.
đ§Ž Parallelism â Exploiting All Cores Like a Pro
Welcome to Pillar #8 of theConcurrency & Multithreading: The Ultimate Engineerâs Bibleseries.
đâ Previous: Immutability⢠đ âNext: Thread Lifecycle & Management*
đ*Parent Blog: The Ultimate Concurrency & Multithreading Guide
â Read for free:Parallelism â Exploiting All Cores Like a Pro
If it helped you, clap once. If it helped a lot, clap more. Thatâs how it reaches others too.
đ§ Imagine This:
Youâve got 16 cores.
You run a program that uses only one.
Youâre wasting 93.75% of your machine.
Parallelism is about doing multiple things at the same time â not just having threads, but designing your logic to actually split and conquer.
Letâs break down how to unleash this in Java.
đ§ Analogy: Cutting Vegetables at a Party
Concurrencyis one knife being passed around â people take turns.Parallelismis 4 people each with their own knife, cutting different vegetables at the same time.
This is not about sharing. This is about dividing and conquering.
đ¨ Fork/Join Framework
Think of this as Javaâs built-in âdivide and conquerâ toolkit. Imagine you have a massive pile of work â instead of doing it all yourself, you break it into smaller chunks, give each chunk to a different worker, and then combine all their results at the end.
This is exactly what Fork/Join does:
- Fork= âHey, go work on this piece separatelyâ
- Join= âWait for everyone to finish, then combine resultsâ
đ§ą Core Components
- ForkJoinPoolâ Think of this as your team manager who assigns work to available workers
- **RecursiveTask
**â A job that breaks itself into smaller jobs AND returns a result (like calculating a sum) - RecursiveActionâ A job that breaks itself into smaller jobs but doesnât return anything (like updating a database)
The âRecursiveâ part means the task can keep breaking itself down until the pieces are small enough to handle easily.
â Example: Sum of Array Using Fork/Join
Letâs say you want to add up all numbers in a huge array. Instead of going through each number one by one, you can:
- Split the array in half
- Calculate the sum of each half separately (in parallel)
- Add those two sums together
public class ArraySumTask extends RecursiveTask<Long> { private final long[] array; private final int start, end; private static final int THRESHOLD = 1000; // If chunk is smaller than this, just do it directly public ArraySumTask(long[] array, int start, int end) { this.array = array; this.start = start; this.end = end; } @Override protected Long compute() { if (end - start <= THRESHOLD) { // Small enough - just calculate directly long sum = 0; for (int i = start; i < end; i++) sum += array[i]; return sum; } else { // Too big - split it in half int mid = (start + end) / 2; ArraySumTask left = new ArraySumTask(array, start, mid); ArraySumTask right = new ArraySumTask(array, mid, end); left.fork(); // Send left half to another thread long rightResult = right.compute(); // Calculate right half on current thread long leftResult = left.join(); // Wait for left half to finish return leftResult + rightResult; } }}
đ Usage:
ForkJoinPool pool = new ForkJoinPool();long sum = pool.invoke(new ArraySumTask(array, 0, array.length));
The beauty? If you have 8 cores, this task can theoretically run 8 times faster than the single-threaded version!
đ Parallel Streams
This is theeasiestway to add parallelism to your Java code. Itâs like having a magic.parallel()button that makes your data processing faster.
List<Integer> numbers = List.of(1, 2, 3, 4, 5);int sum = numbers.parallelStream() // Make it parallel .mapToInt(i -> i * 2) // Double each number .sum(); // Add them all up
What happens behind the scenes? Java automatically:
- Splits your list into chunks
- Processes each chunk on different cores
- Combines the results
- Uses the same ForkJoinPool we talked about above!
â ď¸ Important Gotchas:
- Only use for CPU-heavy work: If youâre just reading files or making web requests, parallelism wonât help much
- Need enough data: Parallel processing has overhead. For tiny lists (like 10 items), itâs actually slower!
- Think big: Works best with hundreds or thousands of items
đ§Š The Safe Operations: .map(), .reduce(), .collect()
These stream operations work great in parallel, but only if you follow the golden rule:donât mess with shared data.
// â
SAFE - each operation is independentint total = numbers.parallelStream() .map(i -> i * i) // Square each number .reduce(0, Integer::sum); // Add them up// â DANGEROUS - multiple threads writing to same variableint badCounter = 0;numbers.parallelStream() .forEach(i -> badCounter++); // Multiple threads might update at once!
Think of it like this: if 4 people are working on different parts of a puzzle, theyâre fine. But if theyâre all trying to write on the same piece of paper at once, chaos ensues.
đ Spliterator (Advanced Topic)
This is the engine that decides how to split your data for parallel processing. You probably wonât need to create custom ones, but itâs good to know they exist. Think of it as the âsmart slicerâ that knows the best way to cut up your specific type of data.
đ Batch Execution with invokeAll
Sometimes you have a bunch of completely separate tasks that can all run at the same time.invokeAllis perfect for this - itâs like saying âgo do all these things, and let me know when theyâre ALL done.â
ExecutorService pool = Executors.newFixedThreadPool(4);List<Callable<String>> tasks = List.of( () -> "Process file A", () -> "Process file B", () -> "Process file C");List<Future<String>> results = pool.invokeAll(tasks);
Perfect for scenarios like:
- Processing multiple files
- Making several API calls
- Running different calculations
âď¸ Parallelism vs Concurrency (The Key Difference)
This is where many people get confused. Let me clear it up:
Concurrency= Managing multiple things that need to happen (like a juggler keeping multiple balls in the air)Parallelism= Actually doing multiple things simultaneously (like having multiple jugglers each handling their own balls)
- Concurrencyis about dealing with a lot of things at once
- Parallelismis about doing a lot of things at once
You can have concurrency without parallelism (one core handling multiple tasks by switching between them), but parallelism requires multiple cores actually working simultaneously.
đ Real-World Scenario: Image Processing
Hereâs where parallelism really shines. Imagine you run a website and need to resize 1000 user-uploaded photos:
Old way (sequential):
- Resize photo 1 (2 seconds)
- Resize photo 2 (2 seconds)
- âŚcontinue for all 1000 photos
- Total time: 33+ minutes
Parallel way:
- Split photos into 8 groups (you have 8 cores)
- Each core resizes ~125 photos simultaneously
- Total time: ~4 minutes
The work didnât change â you just organized it better. Thatâs the power of parallelism.
đ¤ Common Questions & Interview Deep-Dive
How is ForkJoinPool different from ThreadPoolExecutor?
ThreadPoolExecutoris like a restaurant with a fixed number of waiters â each waiter handles one customer at a time, start to finish.
ForkJoinPoolis like a kitchen with sous chefs â if a chef gets a big task (like preparing 100 steaks), they can break it down and get help from idle chefs, then combine the results.
Key Differences:
- ThreadPoolExecutor: Fixed threads, each handles one complete task
- ForkJoinPool: Dynamic work-stealing, threads can help each other
- Use ThreadPoolExecutor for: Independent tasks (web requests, file processing)
- Use ForkJoinPool for: Recursive, dividable tasks (calculations, tree traversals)
Whatâs this âwork-stealingâ everyone talks about?
Imagine 4 threads working on different parts of a calculation:
- Thread A finishes its chunk early
- Thread B is still working and has a big queue
- Thread A âstealsâ some work from Thread Bâs queue
- Result: Better CPU utilization, faster completion
This happens automatically â you donât code it. Itâs why ForkJoinPool is so efficient for parallel recursive algorithms.
When should I NOT use parallelism?
Donât use it when:
- Small datasets(< 1000 elements) â overhead costs more than benefits
- I/O heavy tasksâ threads will just wait for disk/network anyway
- Sequential dependenciesâ step B needs step Aâs result
- Shared mutable stateâ youâll spend more time on synchronization than actual work
Rule of thumb: If your task doesnât fully utilize one CPU core, parallelism wonât help.
How does .parallelStream() work internally?
When you call.parallelStream():
- Spliteratorbreaks your collection into chunks
- Uses thecommon ForkJoinPool(default size = number of cores â 1)
- Each thread processes its chunk through the pipeline (.map, .filter, etc.)
- Collectorcombines results from all threads
// This happens behind the scenes:numbers.parallelStream() // Split into chunks .map(i -> i * 2) // Each thread processes its chunk .collect(toList()); // Combine all chunks back together
Whatâs the common ForkJoinPool?
Java creates ONE shared ForkJoinPool for all parallel streams in your application. Size =Runtime.getRuntime().availableProcessors() - 1.
Why -1?Leaves one core for your main application thread and other system processes.
You can change it with:System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "8")
Fork/Join vs CompletableFuture â when to use what?
ForkJoinPool: Best forrecursive, CPU-boundtasks you can divide mathematically
- Array processing, tree traversals, mathematical calculations
CompletableFuture: Best forI/O-bound, asynchronousworkflows with dependencies
- API calls, database operations, complex async pipelines
Think of it as:
- ForkJoin = âSplit this math problemâ
- CompletableFuture = âDo these steps, but some can happen at the same timeâ
What happens if I nest parallel streams?
// DON'T DO THISlist.parallelStream() .map(item -> item.getChildren().parallelStream() // Nested parallel! .mapToInt(Child::getValue) .sum()) .collect(toList());
Problem: All parallel streams share the same ForkJoinPool. Nested parallelism can cause thread starvation and actually make things slower.
Solution: Only parallelize the outermost operation, or use custom ForkJoinPools.
How do I debug parallel code?
The Challenge: Multiple threads, non-deterministic execution order, race conditions.
Debugging Tips:
- Start with sequential version first
- Use thread-safe logging:
System.out.printlnis thread-safe - Add thread names to logs:
Thread.currentThread().getName() - Use profilers like JProfiler or VisualVM to see thread activity
- Write unit tests with small, predictable datasets
Memory implications of parallelism?
More threads = more memory:
- Each thread has its own stack (typically 1MB)
- ForkJoinPool creates threads based on core count
- Parallel streams create temporary collections for intermediate results
Monitor heap usagewhen processing large datasets in parallel â you might hit memory limits before CPU limits.
Interview Red Flags â What NOT to say
â âParallelism always makes things fasterâ â âParallelism helps with CPU-bound tasks when you have enough data to offset the overheadâ
â âIâll just add .parallel() everywhereâ â âIâll measure and profile to see where parallelism actually helpsâ
â âThreads are threads, theyâre all the sameâ â âDifferent concurrency tools solve different problems â ForkJoin for divide-and-conquer, ExecutorService for independent tasksâ
đ§ Series Navigation
- đParent Blog: The Ultimate Concurrency & Multithreading Guide
- ⏠ď¸Previous: Immutability
- âĄď¸Next: Thread Lifecycle & Management
đĄ TL;DR
- Use ForkJoinPoolfor recursive âdivide and conquerâ tasks
- Use .parallelStream()for easy, everyday parallelism
- Avoid side effectsin parallel pipelines (donât modify shared variables)
- Use invokeAll()for running multiple independent tasks
- Parallelism is performance goldâ but use it smartly on CPU-heavy work with enough data
In the final blog of this series, weâll close the loop â How threads start, how they die, and how to manage them gracefully. The lifecycle of a thread â thatâs whatâs next.
