Virtual Threads with Quarkus made easy

In this first tutorial about Virtual Threads Mastering Virtual Threads: A Comprehensive Tutorial , we have covered the basics of Virtual Threads. In this article we will learn how Quarkus simplifies the implementation and debugging of Virtual Threads in your application through the @RunOnVirtualThread annotation.

The @RunOnVirtualThreads annotation

The @RunOnVirtualThread annotation tells Quarkus to do something special. Instead of running a certain method on the regular thread it’s on, Quarkus will make a new virtual thread and run it there. Quarkus takes care of setting up this virtual thread.

These virtual threads are temporary, meaning they don’t last forever. The main idea behind @RunOnVirtualThread is to move the work of a certain part of the program to this new temporary thread instead of doing it on the usual thread, like an event-loop or worker thread (used in RESTEasy Reactive).

To make this happen, you simply add the @RunOnVirtualThread annotation to the part of the program you want to move. But there’s a catch: it only works if the Java Virtual Machine (JVM) you’re using supports virtual threads (like Java 19 or newer). If it does, then the part you marked with the annotation gets moved to a virtual thread. This way, you can do things that might take some time (blocking operations) without slowing down the regular thread where the virtual thread is connected to.

In the case of RESTEasy Reactive, you can only use this annotation on parts of the program that are either marked as @Blocking or are considered blocking based on how they’re written.

Before diving into an example, let’s recap the requirements to have this annotation as driver for Virtual Threads:

Firstly, you need to use Java 19 or newer. You can enforce it in your pom.xml as follows:

<properties>
    <maven.compiler.source>19</maven.compiler.source>
    <maven.compiler.target>19</maven.compiler.target>
</properties>

Then, you need to use a reactive API in your dependencies. For example:

<dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>

Finally, you need to apply the annotation on blocking methods (explicitly with @Blocking or implicitly blocking). For example:

@GET
@RunOnVirtualThread
public List<Ticket> get() {
        System.out.printf("Called on %s", Thread.currentThread());
        return Ticket.listAll(Sort.by("name"));
}

A sample Quarkus application running with Virtual Threads

Next step will be playing a bit with the @RunOnVirtualThreads annotation on a complete REST application. You can find the full source code at the end of this article, however the main Resource is the following REST API:

@Path("tickets")
@ApplicationScoped
@Produces("application/json")
@Consumes("application/json")
@RunOnVirtualThread
public class MyService {

    @GET

    public List<Ticket> get() {
        System.out.printf("Called on %s", Thread.currentThread());       
        return Ticket.listAll(Sort.by("name"));
    }

   
    @GET
    @Path("{id}")
    public Ticket getSingle(@PathParam(value = "1") Long id) {
        System.out.printf("Called on %s \n", Thread.currentThread());
        Ticket entity = Ticket.findById(id);
        if (entity == null) {
            throw new WebApplicationException("Ticket with id of " + id + " does not exist.", 404);
        }
        return entity;
    }

    @POST
    @Transactional
    public Response create(Ticket ticket) {
        if (ticket.id != null) {
            throw new WebApplicationException("Id was invalidly set on request.", 422);
        }

        ticket.persist();
        return Response.ok(ticket).status(201).build();
    }

    @PUT
    @Path("{id}")
    @Transactional
    public Ticket update(@PathParam(value = "1") Long id, Ticket ticket) {
        if (ticket.name == null) {
            throw new WebApplicationException("Ticket Name was not set on request.", 422);
        }

        Ticket entity = Ticket.findById(id);

        if (entity == null) {
            throw new WebApplicationException("Ticket with id of " + id + " does not exist.", 404);
        }

        entity.name = ticket.name;

        return entity;
    }

    @DELETE
    @Path("{id}")
    @Transactional
    public Response delete(@PathParam(value = "1") Long id) {
        Ticket entity = Ticket.findById(id);
        if (entity == null) {
            throw new WebApplicationException("Ticket with id of " + id + " does not exist.", 404);
        }
        entity.delete();
        return Response.status(204).build();
    }    
}

As you can see, this is a simple CRUD application from our Panache + REST tutorial: Quarkus CRUD Example with Panache Data

I have just added the @RunOnVirtualThreads annotation at class level so that all methods will run through Virtual Threads. Also, for debugging purposes, I’m printing the Thread name to verify that the method is running with Virtual Threads.

When running applications with Virtual Threads you may want to set the following property to have a custom prefix for your Virtual Threads:

quarkus.virtual-threads.name-prefix=virtual_thread

When we run our application and we request a Resource (for example, http://localhost:8080/tickets) we can see on the Console the Thread name, which is indeed a Virtual thread:

Called on VirtualThread[#165,virtual_thread0]/runnable@ForkJoinPool-1-worker-1

Besides, you can also have a look inside the Quarkus Threads, for example through JVisualVM:

The Pitfall of Pinned Threads

Virtual Threads greatly simplify coding reactive applications. On the other hand, overuse of this pattern could lead to pinning virtual threads if your code pins the carrier thread.

For example, that would happen if you include the thread in a synchronized block:

@GET
@RunOnVirtualThread
public List<Ticket> get() {
        pinTheCarrierThread();
        return Ticket.listAll(Sort.by("name"));
}


private void pinTheCarrierThread() {
		synchronized (this) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException ignored) {
				// For testing purpose only.
			}
		}
}

One way to detect the usage of Pinned Threads is using the jdk.tracePinnedThreads option at start up. For example, to use it with Maven:

<plugin>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>${surefire-plugin.version}</version>
  <configuration>
      <systemPropertyVariables>
        <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
        <maven.home>${maven.home}</maven.home>
      </systemPropertyVariables>
      <argLine>--enable-preview -Djdk.tracePinnedThreads</argLine>
  </configuration>
</plugin>

Conclusion

This tutorial showed how to use Virtual Threads in Quarkus application to deliver clear and concise reactive code

Source code: You can find the full source code for this example here: https://github.com/fmarchioni/mastertheboss/tree/master/quarkus/virtual-threads