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
73 lines
4.5 KiB
Markdown
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).
|