CompletableFuture代码在Java 8与Java 11中运行差异的原因及正确执行方式咨询
Let's break down your questions clearly—this is a super common gotcha tied to a key change in how Java manages background threads across versions.
1. What Changed Between Java 8 and Java 11 Causing This Difference?
The root cause is the thread type used by the default ForkJoinPool.commonPool():
- In Java 8, threads in the common pool are non-daemon threads. Non-daemon threads keep the JVM alive even after the main thread finishes. That's why your first code works as expected: after returning "Finish", the main thread exits, but the common pool threads keep running until the 1-20 print task completes, then the JVM shuts down normally.
- Starting in Java 9 (and carried through to Java 11), the common pool switched to using daemon threads. Daemon threads are automatically terminated when all non-daemon threads (like your main thread) exit. So in Java 11, as soon as your main thread returns "Finish" and exits, the JVM shuts down immediately—before the
runAsynctask even starts executing.
2. Why Does Explicitly Specifying an ExecutorService Fix the Execution?
When you create your own ExecutorService (e.g., Executors.newFixedThreadPool(10)), the default thread factory generates non-daemon threads. These threads don't get killed when the main thread exits, so they'll stick around to run your runAsync task to completion.
The reason your program hangs after printing 1-10 is because these non-daemon threads remain active—your ExecutorService hasn't been shut down, so its threads are waiting for more tasks instead of terminating.
3. How to Correctly Implement Normal Execution and Termination?
You have a few solid options depending on your requirements:
Option 1: Wait for the Async Task to Complete (If Blocking Is Acceptable)
If you can let the main thread wait for the task to finish before exiting (either in the method or at the application level), use join() on the CompletableFuture:
private static String test2() { CompletableFuture<Void> future = CompletableFuture.runAsync(() -> IntStream.rangeClosed(1, 20).forEach(x -> { try { Thread.sleep(500); System.out.println(x); } catch (InterruptedException e) { e.printStackTrace(); } })); future.join(); // Blocks until the task finishes return "Finish"; }
This ensures the task runs to completion before the JVM exits, and the program terminates cleanly.
Option 2: Manage a Shared ExecutorService with Application-Level Shutdown
Since your ExecutorService is shared across methods, never shut it down in a single method. Instead:
- Treat the executor as a singleton (e.g., using a static instance or dependency injection).
- Add a JVM shutdown hook to gracefully shut down the executor when the application exits:
private static final ExecutorService SHARED_EXECUTOR = Executors.newFixedThreadPool(10); static { // Register a hook to clean up the executor on app exit Runtime.getRuntime().addShutdownHook(new Thread(() -> { SHARED_EXECUTOR.shutdown(); try { // Wait 60 seconds for pending tasks to finish if (!SHARED_EXECUTOR.awaitTermination(60, TimeUnit.SECONDS)) { SHARED_EXECUTOR.shutdownNow(); // Force stop if timeout hits } } catch (InterruptedException e) { SHARED_EXECUTOR.shutdownNow(); } })); } private static String test2() { CompletableFuture.runAsync(() -> IntStream.rangeClosed(1, 10).forEach(x -> { try { Thread.sleep(500); System.out.println(x); } catch (InterruptedException e) { e.printStackTrace(); } }), SHARED_EXECUTOR); return "Finish"; }
The shutdown hook triggers when the JVM is about to exit, giving pending tasks time to complete before shutting down the executor. This keeps the executor available for other methods while ensuring clean program termination.
Option 3: Use Daemon Threads in Your Shared Executor (If Task Completion Isn't Critical)
If you don't mind tasks being terminated if the app exits early, configure your shared executor to use daemon threads. This way, the threads won't keep the JVM alive after the main thread exits:
private static final ExecutorService SHARED_EXECUTOR = Executors.newFixedThreadPool(10, r -> { Thread thread = new Thread(r); thread.setDaemon(true); // Mark threads as daemon return thread; }); private static String test2() { CompletableFuture.runAsync(() -> IntStream.rangeClosed(1, 10).forEach(x -> { try { Thread.sleep(500); System.out.println(x); } catch (InterruptedException e) { e.printStackTrace(); } }), SHARED_EXECUTOR); return "Finish"; }
Note: This will stop tasks mid-execution if the main thread exits early—only use this if partial task execution is acceptable for your use case.
内容的提问来源于stack exchange,提问作者Misa D.

