Multithreading
Concurrency lets programs do several things at once — handle many requests, download files in parallel, keep a UI responsive.
On this page · 5 min · Advanced
On this page
- In simple words
- Learning objectives
- Explanation
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 5 min
- Difficulty
- Advanced
- Updated
- Oct 30, 2026
In simple words#
A thread is a cook in a kitchen. More cooks finish orders faster — but if two cooks grab the same pan at once (shared data), the dish is ruined. Concurrency tools are the kitchen rules that prevent clashes.
- Thread
- A path of execution.
- ExecutorService
- A managed pool of threads.
- Race condition
- Wrong results when threads change shared data at the same time.
Learning objectives#
- Run tasks in parallel with ExecutorService
- Compose async work with CompletableFuture
- Prevent race conditions
- Use virtual threads
Explanation#
Don’t create raw threads for every task; submit tasks to an ExecutorService.
CompletableFuture chains async work: supplyAsync → thenApply → thenCombine → join.
Shared mutable data needs protection: AtomicInteger, ConcurrentHashMap, synchronized or locks. Better: avoid sharing.
Virtual threads (Java 21) are very lightweight — Executors.newVirtualThreadPerTaskExecutor() handles thousands of blocking tasks easily.
Code example: Parallel price checks with virtual threads#
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class Prices {
static int fetchPrice(String shop) throws InterruptedException {
Thread.sleep(500); // pretend this is an HTTP call
return switch (shop) { case "ShopA" -> 54999; case "ShopB" -> 53999; default -> 55999; };
}
public static void main(String[] args) throws Exception {
AtomicInteger calls = new AtomicInteger();
long start = System.currentTimeMillis();
try (ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<Integer>> results = pool.invokeAll(
List.of("ShopA", "ShopB", "ShopC").stream()
.<Callable<Integer>>map(s -> () -> { calls.incrementAndGet(); return fetchPrice(s); })
.toList());
int best = Integer.MAX_VALUE;
for (Future<Integer> f : results) best = Math.min(best, f.get());
System.out.println("Best price: ₹" + best + " from " + calls.get() + " shops");
}
System.out.println("Took ~" + (System.currentTimeMillis() - start) / 100 * 100 + " ms");
}
}
Best price: ₹53999 from 3 shops Took ~500 ms
This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- Each shop lookup is a Callable task.
- Virtual threads run all three at once.
- AtomicInteger counts calls safely across threads.
- Total time ≈ one call (500 ms), not three.
Real-world use case#
A travel site queries ten airline APIs in parallel and shows results in one second instead of ten.
Common mistakes#
count++ is not atomic. Use AtomicInteger or synchronisation.
Threads keep the app alive. Use try-with-resources (Java 19+) or shutdown().
Best practices#
- Prefer executors over raw threads
- Avoid shared mutable state
- Use concurrent collections and atomics
- Virtual threads for blocking I/O
Remember this#
Tasks → ExecutorService, not new Thread().
Shared data → atomic/concurrent/synchronized.
Java 21: virtual threads for many blocking tasks.
Practice exercise#
Your task: Run two async lookups and combine their results with CompletableFuture.
Show solution
var a = CompletableFuture.supplyAsync(() -> 54999);
var b = CompletableFuture.supplyAsync(() -> 53999);
int best = a.thenCombine(b, Math::min).join();
Mini quiz#
Which class provides a thread-safe counter?
Want more? Practise all Java MCQs · Java interview questions
My notes#
Saved in this browser.