Virtual threads in Java with backwards compatibility
Asked Answered
C

1

5

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).

How can I support virtual threads (where available) but still have the library work correctly in earlier versions of Java?

Carltoncarly answered 20/3 at 9:47 Comment(4)
I guess you could use a factory pattern, and create the right factory based on the detected Java version, combined with a multi-release JAR to hide the Java 21+ code from earlier versions.Lei
meta.#420409Hokkaido
@Hokkaido I interpreted this to be a "How to do XYZ" question, not a "Which ways for doing XYZ are good" question so I don't consider this to be opinion-based. Admittedly, I did originally think about voting to close this question as a duplicate explaining multi-release JARs but I didn't find a good one and (more importantly) realized it's more complicated as there are other ways including but not limited to reflection.Inductive
Just my opinion, but I would not complicate things by solving your problem in two different ways for the two different platforms. I would do everything the old-fashioned way—use a thread pool (Or, if it's complicated, maybe you'll need more than one thread pool.) Thread pools still work just as well as they ever did. Save the virtual threads for when you can develop code that only needs to run on newer JVMs. Supporting different release versions for different platforms is a drag.Miscalculate
I
10

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).

Inductive answered 20/3 at 9:57 Comment(4)
Alternatively, the ThreadFactory abstraction can be used.Sheaff
Yes but if they want to use virtual threads, they probably want to limit concurrency with platform threads.Inductive
They probably want to limit concurrency in either case but in case of virtual threads, the limit should not be enforced by limiting the number of threads but restricting access to the resources, e.g. with a Semaphore. Such changes in programming patterns are the bigger challenge, e.g. avoid blocking calls for platform threads but use them with virtual threads…Sheaff
Yeah that's why I added the isVirtual method in the multi-release JAR version.Inductive

© 2022 - 2024 — McMap. All rights reserved.