# 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` 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).