ExecutorService vs Casual Thread Spawner

While the question and the sample code do not correlate, I’ll try clarifying both.

The advantage of ExecutorService over haphazardly spawning threads is that it behaves predictably and avoids the overhead of thread creation, which is relatively big on the JVM (it needs to reserve memory for each thread, for example).

By predictability, I mean you can control the number of concurrent threads, and you know when and how they might get created and destroyed (so your JVM won’t blow up in case of sudden peaks, and threads won’t be left abandoned leaking memory). You can pass an ExecutorService instance around so that various parts of your program can submit tasks to it, while you still manage it in a single location, fully transparently. An ExecutorService can also be precisely scoped, and shut down when the scope is exited (via shutdown()).

A fixedThreadPool uses a pool of threads that won’t grow beyond what it is allocated.

A cachedThreadPool doesn’t have a max, but will reuse cached threads for a period of time. It’s mostly used in cases where many small tasks need to be executed on a separate thread.

A singleThreadExecutor is for async tasks executed serially.

There are others, like newScheduledThreadPool for periodically repeating tasks, newWorkStealingPool for tasks that can be forked into subtasks and a few others. Explore the Executors class for the details.

As of JVM 18, virtual threads are a thing, and these can be created cheaply, so they change the picture significantly. An ExecutorService that creates a new virtual thread each time can be obtained via newVirtualThreadPerTaskExecutor. The advantage of using this versus spawning threads manually is not performance-centric as much as structural in that it allows for scoping and other benefits explained above, and interoperability with existing APIs that expect an ExecutorService.

Now, on the topic of Runnable vs Callable, it is easy to see from your examples. Callables can return a value place-holder (Future) that will eventually be populated by an actual value in the future. Runnables can not return anything. Additionally, a Runnable also can’t throw exceptions, while a Callable can.

Leave a Comment