Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When to use platform Threads over Virtual Threads

Java 21 introduced the lightweight virtual threads API. Every tutorial I encountered describes them as "as, or more scaleable" then the common platform threads.

I understand that virtual threads run on top of platform threads, but just from the user perspective I want to know if there are now any reasons left to directly use platform threads, for example for ExecutorServices.

I'm not interested in opions but concrete examples like insertion in the debate between Linked- and ArrayList.

like image 629
xtay2 Avatar asked Apr 29 '26 17:04

xtay2


2 Answers

tl;dr

  • Choose virtual threads unless making heavy use of native code or CPU-bound code. (Or making heavy use of synchronized in Java 21, 22, 23.)
  • As needed, throttle your virtual threads with Semaphore (or some such).

Details

Virtual threads are very efficient with both memory and CPU. Common computer hardware can support over a million virtual threads.

Virtual threads should be your default choice rather than platform threads.

Virtual threads are contraindicated in any of these situations:

  • Your task has long-running or frequently-called native code (Foreign Functions, JNI, etc.).
  • Your task is CPU-bound, without blocking, meaning no logging, no file I/O, no database access, no sockets, no network or Web Services calls. For example, video encoding/decoding.
  • (Before Java 24) Your task has long-running or frequently-called code that uses synchronized.

If your task involves any of those situations, use platform threads.

(Occasional brief use of either synchronized or native code in a virtual thread is not a problem. Just avoid deadlocks, etc.)

The Project Loom team removed that limitation involving synchronized in Java 24 and later. See JEP 491: Synchronize Virtual Threads without Pinning.

Of course, “cheap” threads may perform expensive tasks. If your task involves a scarce or heavy resource, throttle your many virtual threads to make limited usage.

Throttling is commonly performed with Semaphore. Use try-finally to never lose a permit.

static final Semphore limitAccessToFooSemaphore = new Semaphore ( 7 ) ;  // Specify number of permits. Or use `1` for a binary semaphore. 
// Your task code, a `Runnable` or `Callable` implementation.
… do some inexpensive work
try
{
    limitAccessToFooSemaphore.acquire() ;  // Blocks until a permit becomes available.
    … do expensive work 
}
finally
{
    limitAccessToFooSemaphore.release() ;  // Return the permit to the semaphore’s pool. 
}
… do more inexpensive work

Before virtual threads, some programmers got into the bad habit of using limited-size thread pools as a way of indirectly throttling access to scarce resources or limit expensive work. Better to manage those directly and explicitly via Semaphore or some such.


For more details, see the excellent video presentations at YouTube by Alan Bateman, Ron Pressler, or José Paumard.

See especially the latest by Alan Bateman where he explains that virtual threads are not “pixie dust”, correcting some misconceptions that have risen within the community.

And read JEP 444!

like image 102
Basil Bourque Avatar answered May 01 '26 06:05

Basil Bourque


What does it mean to be "Using platform threads"

This should not mean "new Thread(...)".

Lets create an example. I am going to crawl a website, each request is going to require some IO. This io will consume a thread, but not use the CPU heavily.

executor.submit( () ->{
    List<Runnable> next = crawlWebsite();
    next.forEach( executor::submit );
});

crawWebsite is just a method that downloads the website parses it and finds the next URLs to visit.

If crawlWebsite gets stuck on the http request, then the os is free to prioritize another thread. If your executor has a fixed number of threads then they can all be stuck on IO? All of your processors are free, but you cannot start a new task because all of your threads are blocking.

Why not just create more threads?

Threads are "expensive". Some thread pools are built on the principle of creating more threads, like Executors.newCachedThreadPool. That works great, unless you are creating thousands of threads. Then the overhead becomes prohibitive.

That is what virtual threads are to address. Cheap actions that might block for a long time without consuming much cpu. Then you can submit the to Executors.newVirtualThreadPerTask. The threads are created used and forgotten. It gets around the finite threadpool problem, and it gets around the overhead of creating platform threads.

  • It will not help if your tasks are CPU bound.
  • Virtual threads can become pinned for certain tasks (esp synchronized block)

I'm not really convinced you shouldn't just use virtual threads instead of platform threads. One instance would be to limit the number of threads you use for a set of tasks.

like image 37
matt Avatar answered May 01 '26 06:05

matt