Intro to virtual threads: A new approach to Java concurrency

0

One particular of the most far-achieving Java 19 updates is the introduction of digital threads. Virtual threads are part of Project Loom, and are offered in Java 19 as a preview.

How digital threads do the job

Virtual threads introduce an abstraction layer amongst running-technique procedures and software-amount concurrency. Said in another way, virtual threads can be employed to agenda tasks that the Java virtual machine orchestrates, so the JVM mediates concerning the functioning method and the method. Figure 1 demonstrates the architecture of digital threads.

Architecture of virtual threads in Java. IDG

Determine 1. The architecture of virtual threads in Java.

In this architecture, the application instantiates digital threads and the JVM assigns the compute sources to take care of them. Contrast this to common threads, which are mapped directly onto running program (OS) processes. With conventional threads, the software code is liable for provisioning and dispensing OS methods. With virtual threads, the application instantiates virtual threads and therefore expresses the will need for concurrency. But it is the JVM that obtains and releases the assets from the working system.

Virtual threads in Java are analogous to goroutines in the Go language. When utilizing digital threads, the JVM is only in a position to assign compute assets when the application’s virtual threads are parked, which means that they are idle and awaiting new do the job. This idling is frequent with most servers: they assign a thread to a ask for and then it idles, awaiting a new celebration like a reaction from a datastore or more input from the community.

Using common Java threads, when a server was idling on a request, an operating method thread was also idling, which seriously confined the scalability of servers. As Nicolai Parlog has defined, “Running units just cannot boost the performance of system threads, but the JDK will make improved use of them by severing the one particular-to-1 marriage between its threads and OS threads.”

Prior attempts to mitigate the effectiveness and scalability difficulties involved with standard Java threads contain asynchronous, reactive libraries like JavaRX. What’s distinct about digital threads is they are executed at the JVM stage, and yet they suit into the existing programming constructs in Java.

Employing Java digital threads: A demo

For this demonstration, I’ve produced a uncomplicated Java application with the Maven archetype. I’ve also built a couple adjustments to empower digital threads in the Java 19 preview. You is not going to want to make these alterations after virtual threads are promoted out of preview. 

Listing 1 reveals the changes I made to the Maven archetype’s POM file. Take note that I also established the compiler to use Java 19 and (as revealed in Listing 2) included a line to the .mvn/jvm.config.

Listing 1. The pom.xml for the demo software



  UTF-8
  19
  19


  org.apache.maven.plugins
  maven-compiler-plugin
  3.10.1
  
    
      --increase-modules=jdk.incubator.concurrent
      --enable-preview
    
  

The --allow-preview switch is required to make exec:java work with preview enabled. It starts off the Maven approach with the required switch.

Listing 2. Incorporating empower-preview to .mvn/jvm.config


--permit-preview

Now, you can execute the plan with mvn compile exec:java and the virtual thread capabilities will compile and execute.

Two approaches to use virtual threads

Now let’s take into consideration the two main approaches you are going to essentially use digital threads in your code. While virtual threads present a remarkable adjust to how the JVM performs, the code is really really related to typical Java threads. The similarity is by design and style and would make refactoring existing apps and servers reasonably simple. This compatibility also signifies that present applications for monitoring and observing threads in the JVM will do the job with digital threads.

Thread.startVirtualThread(Runnable r)

The most standard way to use a virtual thread is with Thread.startVirtualThread(Runnable r). This is a replacement for instantiating a thread and calling thread.get started(). Look at the sample code in Listing 3.

Listing 3. Instantiating a new thread


deal com.infoworld

import java.util.Random

community course Application 
  public static void major( String[] args ) 
    boolean vThreads = args.size > 
    Process.out.println( "Making use of vThreads: " + vThreads)

    long start = Process.currentTimeMillis()

    Random random = new Random()
    Runnable runnable = () ->  double i = random.nextDouble(1000) % random.nextDouble(1000)    
    for (int i =  i < 50000 i++)
      if (vThreads) 
        Thread.startVirtualThread(runnable)
       else 
        Thread t = new Thread(runnable)
        t.start()
      
    
   
    long finish = System.currentTimeMillis()
    long timeElapsed = finish - start
    System.out.println("Run time: " + timeElapsed)
  

When run with an argument, the code in Listing 3 will use a virtual thread otherwise it will use conventional threads. The program spawns 50 thousand iterations of whichever thread type you choose. Then, it does some simple math with random numbers and tracks how long the execution takes.

To run the code with virtual threads, type: mvn compile exec:java -Dexec.args="true". To run with standard threads, type: mvn compile exec:java. I did a quick performance test and got the results below:

  • With virtual threads: Runtime: 174
  • With conventional threads: Runtime: 5450

These results are unscientific, but the difference in runtimes is substantial.

There are other ways of using Thread to spawn virtual threads, like Thread.ofVirtual().start(runnable). See the Java threads documentation for more information.

Using an executor

The other primary way to start a virtual thread is with an executor. Executors are common in dealing with threads, offering a standard way to coordinate many tasks and thread pooling.

Pooling is not required with virtual threads because they are cheap to create and dispose of, and therefore pooling is unnecessary. Instead, you can think of the JVM as managing the thread pool for you. Many programs do use executors, however, and so Java 19 includes a new preview method in executors to make refactoring to virtual threads easy. Listing 4 show you the new method alongside the old.

Listing 4. New executor methods


ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor() // New method
ExecutorService executor = Executors.newFixedThreadPool(Integer poolSize) // Old method

In addition, Java 19 introduces the Executors.newThreadPerTaskExecutor(ThreadFactory threadFactory) method, which can take a ThreadFactory that builds virtual threads. Such a factory can be obtained with Thread.ofVirtual().factory().

Best practices for virtual threads

In general, because virtual threads implement the Thread class, they can be used anywhere that a standard thread would be. However, there are differences in how virtual threads should be used for best effect.  One example is using semaphores to control the number of threads when accessing a resource like a datastore, instead of using a thread pool with a limit. See Coming to Java 19: Virtual threads and platform threads for more tips.

Another important note is that virtual threads are always daemon threads, meaning they'll keep the containing JVM process alive until they complete. Also, you cannot change their priority. The methods for changing priority and daemon status are no-ops. See the Threads documentation for more about this.

Refactoring with virtual threads

Virtual threads are a big change under the hood, but they are intentionally easy to apply to an existing codebase. Virtual threads will have the biggest and most immediate impact on servers like Tomcat and GlassFish. Such servers should be able to adopt virtual threading with minimal effort. Applications running on these server will net scalability gains without any changes to the code, which could have enormous implications for large-scale applications. Consider a Java application running on many servers and cores suddenly, it will be able to handle an order-of-magnitude more concurrent requests (although, of course, it all depends on the request-handling profile).

It may be just a matter of time before servers like Tomcat allow for virtual threads with a configuration parameter. In the meantime, if you are curious about migrating a server to virtual threads, consider this blog post by Cay Horstmann, where he shows the process of configuring Tomcat for virtual threads. He enables the virtual threads preview features and replaces the Executor with a custom implementation that differs by only a single line (you guessed it, Executors.newThreadPerTaskExecutor). The scalability benefit is significant, as he says: “With that change, 200 requests took 3 seconds, and Tomcat can easily take 10,000 requests.”

Conclusion

Virtual threads are a major change to the JVM. For application programmers, they represent an alternative to asynchronous-style coding such as using callbacks or futures. All told, we could see virtual threads as a pendulum swing back towards a synchronous programming paradigm in Java, when dealing with concurrency. This is roughly analogous in programming style (though not at all in implementation) to JavaScript’s introduction of async/await. In short, writing correct asynchronous behavior with simple synchronous syntax becomes quite easy—at least in applications where threads spend a lot of time idling.

Check out the following resources to learn more about virtual threads:

Copyright © 2022 IDG Communications, Inc.

Leave a Reply