Java 21 Virtual Threads vs Java 17 Thread Pools in Swing Applications

Published: 2026-06-17
Google AdSense 广告位 (待申请通过后开启)

The Problem with Virtual Thread Pools

For example, when Swing issues a request, one thread keeps executing, which is very slow. If you want to stop all requests, and the thread is running continuously, you can't stop it once it's started. Currently, the only solution is to exit and restart the entire program. Is there a way to do this without restarting?

No, you don't need to restart. You can manually cancel all running background requests at any time by properly managing your threads.

In Java, directly "killing" a thread is unsafe (thread.stop() is deprecated). The most standard and lossless solution is to use a thread pool (ExecutorService), which allows you to shut down and rebuild the thread pool with a single click when cleanup is needed. Core solution: Use ExecutorService for unified management. You can use Java 21's virtual thread pool. When the user wants to clean up the thread pool with one click, simply shut down the old thread pool (shutdownNow()) and create a new one.

shutdownNow() only sends interruption signals.

java-21 virtual threads
java-21 virtual threads

Tasks that ignore interruption requests may continue running until they complete.

1. Define a global thread manager

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

public class RequestManager {

	// Use volatile to ensure multi-threaded visibility

	private static volatile ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

	// Submit tasks to virtual threads

	public static void submitTask(Runnable task) {

	if (!executor.isShutdown()) {

	executor.submit(task);

	}

	}

// 【Core】Clean up all requests and threads with one click, no restart required

	public static void clearAllRequests() {

	// 1. Forcefully interrupt all running tasks and reject new tasks

	executor.shutdownNow();

	// 2. Recreate a new virtual thread pool so that subsequent requests can continue to be submitted

	executor = Executors.newVirtualThreadPerTaskExecutor();

	System.out.println("All background requests have been successfully cleaned up!");

		}
}

2. Write your time-consuming task (must respond to interrupts) For shutdownNow() to take effect immediately, your background network requests or time-consuming loop code must be able to respond to interrupt signals (InterruptedException or check Thread.currentThread().isInterrupted()).

// Example: Sending a request when a Swing button is clicked

buttonSend.addActionListener(e -> {

		RequestManager.submitTask(() -> {

		try {

		System.out.println("Starting a slow request...");


		// Simulate a long-running operation (such as a network request). If canceled here, an exception will be thrown and the program will exit.

		Thread.sleep(10000);


		// If it's a hard-coded loop, you need to manually check the interrupt status:

		/*
		while (!Thread.currentThread().isInterrupted()) {

		// Execute part of the task...

		}

		*/

		// After the task is successfully completed, switch back to the Swing EDT thread to update the UI

		javax.swing.SwingUtilities.invokeLater(() -> {

		labelResult.setText("Request successful!");

		});

		} catch (InterruptedException ex) {

		// When the "Clean up" button is clicked, the program will reach here

		System.out.println("The request was forcibly canceled by the user.");

		}
		});
});

3. Handling the Swing "One-Click Cleanup" Button Place a "Cancel/Cleanup" button on your Swing interface. Clicking it will allow the user to click:

buttonClear.addActionListener(e -> {

	// Call the cleanup method

	RequestManager.clearAllRequests();

	// Update the UI to notify the user

	labelStatus.setText("All tasks have been cleaned up, and the interface has returned to responsiveness.");

});

Why does this solution perfectly solve your problem? No restart required: shutdownNow() sends an interrupt() signal to all running virtual threads, causing them to exit immediately, thus freeing up CPU and memory. Seamless operation: After cleanup, the newly created executor is clean and usable again, and the user can immediately click the next request.

The interface remains smooth: all cleanup and requests occur in the background, keeping the Swing interface (EDT) constantly active.

public void stopCommandExecutor(KeyEvent e) {

	if (e.getKeyCode() == KeyEvent.VK_X &&

	(e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0) {

	// 1. Forcefully interrupt all currently executing commands (it is recommended to use shutdownNow, which is faster and cleaner than shutdown)

	gitCommandExecutor.shutdownNow();

	// 2. [Core] A new thread pool must be created and assigned to it, otherwise subsequent commands will not be executed

	gitCommandExecutor = Executors.newVirtualThreadPerTaskExecutor();

	// Triggered Ctrl + X

	System.out.println("Ctrl + X was pressed, old threads have been cleaned up and the thread pool has been reset.");

		}
} 

If your class definition has the final keyword before gitCommandExecutor (e.g., private final) If you define `ExecutorService gitCommandExecutor;`, a compilation error will occur when reassigning it in the second step above. The solution is to remove the `final` keyword before `gitCommandExecutor` from the top of the class definition, making it a modifiable variable.

However, this is not entirely possible in Java 17. Directly writing `Executors.newVirtualThreadPerTaskExecutor()` will result in a compilation error.

This is because Virtual Threads were only officially introduced in Java 21.

As an early LTS version, Java 17's `Executors` class does not contain this method.

If you need to implement one-click cleanup and subsequent command execution in a Java 17 environment, you need to use a traditional platform thread pool (CachedThreadPool).

For Java 17 compatibility, please modify your code as follows:

public void stopCommandExecutor(KeyEvent e) {

		if (e.getKeyCode() == KeyEvent.VK_X &&

		(e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0) {

		// 1. Forcefully interrupt all currently executing commands

		gitCommandExecutor.shutdownNow();

		// 2. In Java 17, use CachedThreadPool instead of a virtual thread pool

		// Its characteristic is also to create new threads on demand and automatically recycle threads after the task is completed

		gitCommandExecutor = Executors.newCachedThreadPool();

		System.out.println("Ctrl + X was pressed, old thread pool has been cleaned up (Java 17 compatible version).");

	}
}

Why use CachedThreadPool in Java 17? Is `newCachedThreadPool()` the most suitable? Java 21 Virtual Threads vs. Java 17 Thread Pools: Dynamic scaling: It behaves very similarly to virtual thread pools in Swing scenarios. When a new command comes in, it immediately creates a new thread to run it, preventing the interface from freezing. Perfect compatibility: It's a long-established API from Java 5 and is extremely stable in Java 17. Consistent functionality: When `shutdownNow()` is called, it also sends an interrupt signal to all running background commands, achieving immediate cleanup.

Explore More

Technology Guides →

Building a Postman-Like API Client: Go Fyne vs Java Swing

Building a Linux Command Search Tool with Java Swing

Java Thread to ExecutorService Opmize SwingUI

Oracle vs IBM: Who Would Have Been the Better Steward of Java?

Southeast Asia Insights →