While working on a small JavaFX application, I noticed some strange behavior. When I closed all windows, the application did not stop immediately, even though all threads were done with their work. It became even more mysterious. After a while the application would shut down correctly. What was going on?
As it turned out, the reason was an ExecutorService that used a cached thread pool. Caching and reusing threads is a good idea, in particular if you want to execute many small tasks. Creating threads is expensive and by caching and reusing them, you can avoid the overhead.
What I was not aware of though: the ExecutorService does not remove idle threads immediately, but keeps them for about 60 seconds before clearing them. Further analysis showed that idle threads in the ExecutorService, which were waiting to be removed, prevented the immediate shutdown of my application. The following code example condenses the problem:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.concurrent.ExecutorService; | |
import java.util.concurrent.Executors; | |
public class CachedThreads { | |
public static void main(String[] args) { | |
final ExecutorService executor = Executors.newCachedThreadPool(); | |
executor.submit(() -> System.out.println("Hello World")); | |
} | |
} |
One would expect that the application ends immediately, but it waits about 60 seconds before it returns.
The Solution
In my case the solution was easy. I declared the threads in the thread pool as daemon threads. Daemon threads do not prevent the application from exiting, even if they are still running. You can achieve this by declaring a thread factory and passing it to Executors.newCachedThreadPool() as you can see in the following example. This ensures that all threads of the ExecutorService are daemon threads. In addition, it also allows to name the threads, which is tremendously useful during development.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.concurrent.ExecutorService; | |
import java.util.concurrent.Executors; | |
import java.util.concurrent.ThreadFactory; | |
public class ImprovedCachedThreads { | |
public static void main(String[] args) { | |
final ThreadFactory threadFactory = runnable -> { | |
final Thread thread = new Thread(runnable, "HelloWorldThread"); | |
thread.setDaemon(true); | |
return thread; | |
}; | |
final ExecutorService executor = Executors.newCachedThreadPool(threadFactory); | |
executor.submit(() -> System.out.println("Hello World")); | |
} | |
} |
Unfortunately the example above will still not work correctly. Daemon threads really stop immediately, even if they still have work to do. In the example above, it is very likely, that you do not see the output “Hello World” at all, because the application exits before it can print the output. In my application the problematic threads do not do anything important and I am able to kill them immediately, but that is certainly not always the case. If you need to be sure that a daemon thread finishes its work, you have to test it explicitly, e.g. by using a CountDownLatch.
Another solution is to create the ThreadPoolExecutor directly instead of using one of the factories in Executors. This allows you to modify the keepAliveTime, which defines how long idle threads are kept. But be careful: if the keepAliveTime becomes too small, you end up creating and destroying too many threads.