Learning goals
By the end of this chapter you should be able to:
- Create and start threads and understand the
Runnable/Callablesplit - Use
ExecutorServiceto pool threads and submit tasks - Shut down executors cleanly with
shutdownandawaitTermination - Choose between platform threads and virtual threads (preview of next chapter)
Why concurrency?
Modern servers handle many requests at once. One thread blocked on IO should not force you to spawn an OS thread per socket at massive scale — but you still need a model for parallel work and background tasks.
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> future = pool.submit(() -> heavyCompute(42));
int result = future.get(); // blocks until done
pool.shutdown();
This chapter covers platform threads and executors — the baseline before synchronization and virtual threads.
Thread basics
A thread is a sequential flow of execution sharing process memory (heap) but with its own stack.
Thread t = Thread.ofPlatform().name("worker").start(() -> {
System.out.println(Thread.currentThread().getName());
});
t.join(); // wait for completion
Legacy style still appears in older code:
new Thread(() -> task()).start();
Prefer ExecutorService over raw Thread sprawl — pooling and lifecycle management are built in.
Runnable vs Callable
Runnable job = () -> log.info("no return value");
Callable<String> task = () -> {
return fetchFromDb();
};
Runnable — no return, no checked exceptions in signature.
Callable<V> — returns V, may throw Exception; submit via executor → Future<V>.
ExecutorService patterns
try (ExecutorService exec = Executors.newFixedThreadPool(8)) {
List<Callable<Path>> jobs = files.stream()
.map(f -> (Callable<Path>) () -> process(f))
.toList();
List<Future<Path>> futures = exec.invokeAll(jobs);
for (Future<Path> f : futures) {
Path done = f.get(); // unwrap; ExecutionException wraps task failure
}
}
Common factory methods:
| Factory | Use |
|---|---|
newFixedThreadPool(n) |
bounded worker pool |
newCachedThreadPool() |
unbounded growth — risky under load |
newSingleThreadExecutor() |
sequential tasks, ordered queue |
newVirtualThreadPerTaskExecutor() |
one virtual thread per task (Java 21+) |
Future and CompletableFuture (intro)
Future.get() blocks. Timeout variant:
String value = future.get(2, TimeUnit.SECONDS);
CompletableFuture composes async pipelines (Java 8+):
CompletableFuture.supplyAsync(() -> fetchUser(id))
.thenApply(User::email)
.thenAccept(System.out::println);
Default supplyAsync uses ForkJoinPool.commonPool() — understand that shared resource.
Graceful shutdown
pool.shutdown(); // no new tasks
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
pool.shutdownNow(); // interrupt running
}
Failing to shutdown thread pools prevents JVM exit in small apps and leaks threads in redeployed containers.
Thread factories and naming
Named threads simplify logs and thread dumps:
ThreadFactory factory = Thread.ofPlatform()
.name("worker-", 0)
.factory();
ExecutorService exec = Executors.newFixedThreadPool(4, factory);
Common mistakes
- Unbounded
newCachedThreadPoolunder bursty load — creates unlimited threads → OOM or OS limit. - Calling
get()on the event/request thread without timeout — stalls latency. - Sharing mutable objects without synchronization — race conditions (next chapter).
- Ignoring
ExecutionException— real failure isgetCause(). - Not shutting down executor on app stop — leaked threads.
Practice checkpoint
-
Submit 10 tasks and wait for all — which method? Answer:
invokeAllor submit loop +geton eachFuture. -
Difference between
execute(Runnable)andsubmit(Callable)? Answer:executevoid fire-and-forget;submitreturnsFuturewith result/exception. -
Why avoid creating a new
Threadper request in a server? Answer: OS thread cost and context switching; use pools or virtual threads. -
After
shutdownNow(), are queued tasks guaranteed to run? Answer: no — they are returned as cancelled; running tasks get interrupt.
Interview angles
- Platform thread cost: ~1 MB stack default, kernel scheduling — motivates virtual threads.
- Executor vs manual threads: separation of task submission from thread creation.
- ForkJoinPool.commonPool: shared by parallel streams and default async CF — contention risk.
- Daemon threads: JVM exits when only daemons remain — use for background housekeeping sparingly.
Next: /java/learn/concurrency/synchronization-and-visibility/