ExecutorService
is your friend
First of all, you can use ExecutorService
to your advantage. Once you have an ExecutorService
(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 newVirtualThreadPerTaskExecutor
is available:
ExecutorService executor;
try{
Method method = Executors.class.getMethod("newVirtualThreadPerTaskExecutor");
executor = (ExecutorService) method.invoke(null);
}catch(NoSuchElementException e){
executor = Executors.newFixedThreadpool(10);//or similar
}
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 ExecutorService
and checking whether virtual threads are available:
//normal class
class ThreadCreator{
public ExecutorService createExecutor(){
return Executors.newFixedThreadpool(10);//or similar
}
public boolean isVirtual(){
return false;
}
}
//Java 21
//put this in META-INF/versions/21/your/packagename
class ThreadCreator{
public ExecutorService createExecutor(){
return Executors.newVirtualThreadPerTaskExecutor();
}
public boolean isVirtual(){
return true;
}
}
In order to use multi-release JARs, you need to add Multi-Release: true
to your MANIFEST.MF
.
Furthermore, the class providing the virtual thread ExecutorService
should be compiled with Java 21 and be located in META-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 Semaphore
in 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 own ExecutorService
class that decorates the tasks with a Semaphore
limiting concurrency.
public class ConcurrencyLimitingExecutorService extends AbstractExecutorService {
private final ExecutorService delegate;
private final Semaphore semaphore;
public ConcurrencyLimitingExecutorService(ExecutorService delegate, int concurrency) {
this.delegate = delegate;
this.semaphore = new Semaphore(concurrency);
}
@Override
public void shutdown() {
delegate.shutdown();
}
@Override
public List<Runnable> shutdownNow() {
return delegate.shutdownNow();
}
@Override
public boolean isShutdown() {
return delegate.isShutdown();
}
@Override
public boolean isTerminated() {
return delegate.isTerminated();
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
return delegate.awaitTermination(timeout, unit);
}
@Override
public void execute(Runnable command) {
delegate.execute(()->{
try{
semaphore.acquire();
try {
command.run();
}finally {
semaphore.release();
}
}catch(InterruptedException e){
Thread.currentThread().interrupt();
}
});
}
}
You can then wrap the ExecutorService
for virtual threads with this class.
//if you do it with reflection
ExecutorService executor;
try{
Method method = Executors.class.getMethod("newVirtualThreadPerTaskExecutor");
ExecutorService raw = (ExecutorService) method.invoke(null);
executor = new ConcurrencyLimitingExecutorService(raw, 100);//or choose another limit
}catch(NoSuchElementException e){
executor = Executors.newFixedThreadpool(10);//or similar
}
//if you use multi-release JARs
//Java 21
//put this in META-INF/versions/21/your/packagename
class ThreadCreator{
public ExecutorService createExecutor(){
return new ConcurrencyLimitingExecutorService(Executors.newVirtualThreadPerTaskExecutor(), 100);//or choose another limit
}
public boolean isVirtual(){
return true;
}
}
Options other than ExecutorService
As mentioned in the comments, you can use a ThreadFactory
or 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 using Semaphore
for platform threads would result in you wasting platform threads).
These methods work both with reflection and multi-release JARs (you can use an isVirtual
method in a multi-release JAR or set a boolean
to true
if creating a virtual thread per task ExecutorService
with reflection succeeded).