I am writing a library that makes extensive use of threading and would likely benefit from virtual threads in Java 21+.
However, the library must also work for earlier versions of Java (possibly back as far as Java 11).
What is the best way to support virtual threads (where available) but still have the library work correctly in earlier versions of Java?
ExecutorServiceis your friendFirst of all, you can use
ExecutorServiceto your advantage. Once you have anExecutorService(which you can get for both virtual (if available) and platform threads), you can submit tasks to it.To get the
ExecutorService, I can see two major options options:Reflection
You can use reflection to check whether the method
newVirtualThreadPerTaskExecutoris available:If you want to, you can also add an additional version check for this.
Multi-release JARs
Another option is using multi-release JAR files. This allows you to have multiple versions of the same class and Java will decide which one to use based on the Java version.
You could create a class responsible for obtaining the
ExecutorServiceand checking whether virtual threads are available:In order to use multi-release JARs, you need to add
Multi-Release: trueto yourMANIFEST.MF.Furthermore, the class providing the virtual thread
ExecutorServiceshould be compiled with Java 21 and be located inMETA-INF/versions/21/followed by the package as you would have it normally within your JAR.You may also want to check articles like this one for Maven in order to create the multi-release JAR with the build tool used in your project.
If you want to ensure that these methods are present, you can create an interface declaring the common methods and make the two versions of the class implement the interface.
Limiting concurrency
With platform threads, the preferred approach to limiting concurrency is to limit the amount of threads in a pool (as shown in my examples).
However, this doesn't work well with virtual threads as you don't (or shouldn't) pool them. This is why (as it is also mentioned in the comments), you probably want to use
Semaphorein order to do that if you are using virtual threads (and don't do that with platform threads as you are already limiting the pool size). You could do that by creating your ownExecutorServiceclass that decorates the tasks with aSemaphorelimiting concurrency.You can then wrap the
ExecutorServicefor virtual threads with this class.Options other than
ExecutorServiceAs mentioned in the comments, you can use a
ThreadFactoryor a custom method creating a virtual/platform thread. However, with this approach, you will not get the simple ways of limiting concurrency via the thread pool size for platform threads (and usingSemaphorefor platform threads would result in you wasting platform threads).These methods work both with reflection and multi-release JARs (you can use an
isVirtualmethod in a multi-release JAR or set abooleantotrueif creating a virtual thread per taskExecutorServicewith reflection succeeded).