I am migrating my Spring Boot application to virtual threads from the platform ones. Generally, it works fine, no pinning happens. But for some requestrequests, 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 not end up in a situation in whichwhen all the carriers are pinned and there's no platform thread to serve a request left. The
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.
So,Edited: Here is the followingdemonstration code:
// Some other operations on a virtual thread return obj.thisLeadsToPinning(); Would be rewritten as: (Spring Boot)
// Some other operations on a virtual thread return service.submit(() -> obj.thisLeadsToPinning()).get(); 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 this, the operation now 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 requiresrequire the operation to run.
For me it seems like it wouldn't be bad for the application performance.Are Are my assumptions correct, or am I missing something?