I am migrating my Spring Boot application to virtual threads from the platform ones. Generally, it works fine, no pinning happens. But for some requests, there could be an operation that leads to the pinning (due to a blocking operation inside of a synchronized block) and I want to avoid that. If I don't, I could end up in a situation when all the carriers are pinned and there's no platform thread to serve a request left.
The operation that leads to the pinning is a part of a 3rd-party library. While waiting for the fix that would allow me to execute the library's code without pinning, I am planning to use the following approach:
Submit the code that pins the virtual thread's carrier to an executor service (backed by platform threads) and immediately call .get() (from a virtual thread) on the CompletableFuture I would receive from the executor.
Edited: Here is the demonstration code (Spring Boot)
import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; import java.time.Instant; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @Component class AppRunner implements ApplicationRunner { private final Object monitor = new Object(); private final ExecutorService executorService; public AppRunner() { this.executorService = Executors.newFixedThreadPool(2); } @Override public void run(ApplicationArguments args) { String parallelismNum = System.getProperty("jdk.virtualThreadScheduler.parallelism"); System.out.println("jdk.virtualThreadScheduler.parallelism is set to: " + parallelismNum); try (var virtualExecutor = Executors.newVirtualThreadPerTaskExecutor()) { for (int taskId = 0; taskId < 100; taskId++) { final int taskIdentifier = taskId; System.out.println("Submitting the task to executor: " + taskIdentifier); virtualExecutor.submit(() -> simpleLongOperation(taskIdentifier)); /** * If I submit the following instead of pinningOperationOnPlatform, * My simpleLongOperation would be waiting for a carrier to unpin */ // virtualExecutor.submit(() -> pinningOperation(taskIdentifier)); virtualExecutor.submit(() -> pinningOperationOnPlatform(taskIdentifier)); } } } /** * This runs the pinning operation on a platform thread but awaits on the virtual. * So no pinning is actually happening. */ private void pinningOperationOnPlatform(int id) { try { executorService.submit(() -> pinningOperation(id)) .get(); } catch (Exception e) { throw new RuntimeException(e); } } private void simpleLongOperation(int id) { System.out.println("\tSIMPLE Start operation " + id + " : " + Instant.now()); sleep(1_000); System.out.println("\tSIMPLE Complete operation " + id + " : " + Instant.now()); } private void pinningOperation(int id) { System.out.println("PINNING Start operation " + id + " : " + Instant.now()); synchronized (monitor) { sleep(5_000); } System.out.println("PINNING Complete pinning operation " + id + " : " + Instant.now()); } private void sleep(long millis) { try { Thread.sleep(1_000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } As I understand, the operation could now wait to enter the synchronized block on the executor's pool thread, but since the .get() method is called from a virtual thread, their carrier would be free once the operation is done. Now it seems that I'm limiting my parallelism to the size of the thread pool the executor uses to move the pinning operation on the platform thread to run, but only for the (rare) requests that require the operation to run.
For me it seems like it wouldn't be bad for the application performance. Are my assumptions correct, or am I missing something?
ExecutorService(service) in the code is constructed ofVirtualThreadFactory? If I could suggest, I don't think you would be gaining much from moving to invoking the pinned methods from an executor service making use of a virtual thread factory. Another aspect that I could infer from the details shared is that you seem to be attempting to pool the resources, is it that you want to control the concurrent tasks executed at a given time?