Files
java-core-examples/executors/README.md
T
asmhatreandClaude Sonnet 5 bc5153d2a5 executors: ExecutorService lifecycle companion code
Adds the executors module: shutdown() vs shutdownNow(), the graceful
two-phase shutdown pattern, AutoCloseable/try-with-resources (JDK 19+),
newVirtualThreadPerTaskExecutor() timing vs a bounded pool, and the
submit()-without-get() lost-exception trap. 5 runnable demos, 1 JUnit
correctness test class (23 tests), 6 captured output transcripts.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FhzLY5p6okFva3qsnsRyvM
2026-09-30 06:54:17 +00:00

73 lines
4.5 KiB
Markdown

# executors
Companion code for the ankurm.com post *"ExecutorService Done Right: Shutdown, try-with-resources
and Virtual Thread Executors."* Sixth module in `java-core-examples`, the Java-core / concurrency
series.
Five demos, each isolating one thing people get wrong about `ExecutorService` lifecycle and task
submission: `shutdown()` vs `shutdownNow()`, the two-phase graceful-shutdown pattern, the
`AutoCloseable` try-with-resources shortcut (JDK 19+), what `newVirtualThreadPerTaskExecutor()`
actually costs against a bounded pool doing the same blocking work, and the exception that
`submit()` quietly swallows if nothing ever calls `Future.get()`.
## Versions this was built and tested against
| Component | Version | Notes |
|---|---|---|
| JDK | 25.0.4.1+1 (Temurin, LTS) | Every demo here runs on one JDK - no cross-version comparison needed. |
| JUnit Jupiter | 5.11.0 | Correctness tests, 10 repeats for the two timing-sensitive ones. |
| Maven | 3.9.11 | |
| Hardware | 2 vCPU x86-64 VM | Same sandbox as the rest of this series. |
## Quickstart
```bash
export JAVA_HOME=/path/to/jdk-25
mvn package
java -cp target/classes com.ankurm.executors.ShutdownVsShutdownNowDemo
java -cp target/classes com.ankurm.executors.VirtualThreadPerTaskExecutorDemo
```
`scripts/run-all.sh` regenerates every file in `output/` (needs `JDK25_HOME`).
## What's in here
| File | What it shows |
|---|---|
| `.../ShutdownVsShutdownNowDemo.java` | `shutdown()` finishes everything queued; `shutdownNow()` interrupts what's running and abandons what's still waiting. |
| `.../GracefulShutdownPatternDemo.java` | The two-phase pattern from the `ExecutorService` javadoc: `shutdown()`, bounded `awaitTermination()`, escalate to `shutdownNow()` only if the grace period runs out. |
| `.../AutoCloseableExecutorDemo.java` | Since JDK 19, `ExecutorService` extends `AutoCloseable` - `close()` runs that same two-phase pattern for you, so try-with-resources is a correct one-liner, not a shortcut that skips it. |
| `.../VirtualThreadPerTaskExecutorDemo.java` | 5,000 sleeping tasks against `newVirtualThreadPerTaskExecutor()` vs a 20-thread fixed pool - real wall-clock numbers, not a claim. |
| `.../LostExceptionTrapDemo.java` | `execute()` lets an exception reach the default uncaught-exception handler and print. `submit()` without a later `get()` swallows the identical exception completely. |
| `src/test/.../ExecutorShutdownBehaviorTest.java` | Asserts `shutdown()` drops nothing, `shutdownNow()` abandons exactly the still-queued tasks, try-with-resources blocks until every task is done, the virtual-thread executor really runs on virtual threads, and a `submit()`'d exception only surfaces at `get()`. |
| `output/01`-`05` | Each demo's real run. |
| `output/06` | JUnit correctness run. |
## Reading the results honestly (2-vCPU sandbox)
**`shutdownNow()` doesn't guarantee interruption, it requests it.** `output/01` shows both running
tasks stopping mid-`sleep()` because `Thread.sleep()` responds to `interrupt()` - a task that never
checks `Thread.interrupted()` or calls an interruptible blocking method would keep running to
completion regardless of `shutdownNow()`. The 4 queued-but-not-started tasks are handled
differently: `shutdownNow()` returns them directly as a `List<Runnable>` and they never run at all.
**The virtual-thread number here (`output/04`) is the whole argument, not a summary of it.** 5,000
tasks sleeping 50ms each finished in 121ms on `newVirtualThreadPerTaskExecutor()` - close to the
cost of one task, because nothing waited for a worker to free up. The identical workload on a
20-thread fixed pool took 12,556ms, in line with the ~250 sequential rounds of 20 that a bounded
pool forces. This isn't "virtual threads are faster" in general - it's that a large number of
*blocking* tasks stop competing for a small thread count once the thread stops being the scarce
resource.
**The `submit()` silence in `output/05` is real, not a simplification.** The failing task's
`Future.isDone()` reports `true` immediately, and nothing about the program's output changes
whether or not that Future is ever inspected. Compare that block to the `execute()` block right
above it in the same transcript, on the same pool: identical exception, and one prints a full stack
trace unprompted while the other prints nothing at all until something explicitly calls `get()`.
That gap is exactly why a task silently "not running" in production logs is worth checking for an
un-retrieved `Future` before anything else.
## License
MIT - see the [repo-wide LICENSE](../LICENSE).