Virtual and Process threads with Spring Boot

Spring Boot 3.2 introduced Virtual thread support along with Java 21. Virtual threads in JAVA introduced the application level concurrency in a different paradigm. Imagine a boot application fielding around 50K blocking requests and not getting an out-of-memory error.

In the current world, when thinking about threads in Java/Boot programming, we are limited to the number of threads a machine/hardware can support. Java supported multiple threads using the concurrency APIs. When running Java applications, we always had to keep the CPU capacity and memory in mind when spinning threads.

Java introduced project LOOM in earlier versions and as of JAVA 21 it is official,

Virtual threads are derived from the JRE we are running; these are not the OS threads that programs are used to. The base class from virtual threads is the Continuation JAVA library.

Let us take the picture above,

  • HTTP request 1 fires from the browser
  • JRE determines that a virtual thread is needed
  • Kicks off a new Virtual thread
  • HTTP request 2 is fired from the browser
  • In parallel the Virtual thread kicks off a blocking call
  • Performs the blocking call operation
  • Returns the Virtual thread operation back to OS thread
  • In this whole sequence JRE takes care of tracking the virtual thread and returns response.

Boot with version 3.2 officially supports the virtual thread. This makes the developers’ work much more manageable when handling high-throughput DB/IO operations.

Developer onboarding to a virtual thread is accessible in the boot framework; the code entry in application .yml allows the boot to start spinning the virtual thread provided by TOMCAT.

spring:
threads:
virtual:
enabled: true

Once the framework is indicated about the virtual thread, any new thread that is being spun is now offloaded to the JRE to kick off a virtual thread. So, for example, in this code block, after adding the above spring boot property

@Transactional
public GarminRunResponse saveGarminRun(GarminRunRequest garminRunRequest) {
  for (int i = 0; i < 10000; i++) {
        new Thread(() -> {
         System.out.println("Inserting from the virtual thread" +Thread.currentThread().getName());
handleUserRequest(garminRunRequest);}).start();
    }
        return handleUserRequest(garminRunRequest);
    }

private GarminRunResponse handleUserRequest(GarminRunRequest garminRunRequest) {
        GarminRun garminRun = garminRunMapper.toEntity(garminRunRequest);
   garminRun.setId(RandomGenerator.getDefault().nextLong());
GarminRun savedGarminRun = garminRunRepository.save(garminRun);
     return garminRunMapper.toResponse(savedGarminRun);
    }

In the code snippet above, we insert around 10K records to the DB- blocking call and ask the JRE to offload that blocking call to a virtual thread. JRE offloads the new thread call with a virtual thread. Boot knows this because it is called out in the application properties file, as discussed earlier. These iterative steps of concurrency are handled in the modern JAVA JRE. With this type of non-blocking capability given to develop code, functional/reactive programming is made more intuitive. As a development community, it is up to us to either pick the reactive store or use the JAVA LOOM project to make concurrency/non-blocking APIs easier to adopt.

All the code that is discussed in the blog post is in GITHUB repo