Intro to virtual threads: A new approach to Java concurrency

Intro to virtual threads: A new approach to Java concurrency

One of the most much-achieving Java 19 updates is the introduction of virtual threads. Virtual threads are aspect of Venture Loom, and are obtainable in Java 19 as a preview.

How digital threads perform

Virtual threads introduce an abstraction layer amongst functioning-program procedures and application-stage concurrency. Said differently, digital threads can be employed to program duties that the Java virtual equipment orchestrates, so the JVM mediates between the functioning process and the method. Figure 1 displays the architecture of virtual threads.

Architecture of virtual threads in Java. IDG

Determine 1. The architecture of virtual threads in Java.

In this architecture, the software instantiates virtual threads and the JVM assigns the compute resources to handle them. Contrast this to standard threads, which are mapped specifically on to functioning procedure (OS) processes. With conventional threads, the software code is dependable for provisioning and dispensing OS resources. With digital threads, the software instantiates virtual threads and thus expresses the require for concurrency. But it is the JVM that obtains and releases the methods from the operating system.

Digital threads in Java are analogous to goroutines in the Go language. When making use of digital threads, the JVM is only ready to assign compute resources when the application’s digital threads are parked, that means that they are idle and awaiting new work. This idling is prevalent with most servers: they assign a thread to a ask for and then it idles, awaiting a new function like a reaction from a datastore or further input from the network.

Utilizing typical Java threads, when a server was idling on a ask for, an working procedure thread was also idling, which seriously confined the scalability of servers. As Nicolai Parlog has described, “Running units can’t enhance the effectiveness of platform threads, but the JDK will make improved use of them by severing the a person-to-1 relationship involving its threads and OS threads.”

Prior endeavours to mitigate the performance and scalability troubles affiliated with conventional Java threads include things like asynchronous, reactive libraries like JavaRX. What’s different about virtual threads is they are carried out at the JVM stage, and however they match into the existing programming constructs in Java.

Using Java digital threads: A demo

For this demonstration, I have produced a easy Java software with the Maven archetype. I’ve also produced a few improvements to help digital threads in the Java 19 preview. You would not require to make these adjustments at the time virtual threads are promoted out of preview. 

Listing 1 exhibits the modifications I manufactured to the Maven archetype’s POM file. Note that I also set the compiler to use Java 19 and (as demonstrated in Listing 2) added 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
  
    
      --incorporate-modules=jdk.incubator.concurrent
      --help-preview
    
  

The --help-preview switch is needed to make exec:java work with preview enabled. It starts off the Maven method with the necessary swap.

Listing 2. Adding enable-preview to .mvn/jvm.config


--permit-preview

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

Two means to use virtual threads

Now let us look at the two most important approaches you will basically use virtual threads in your code. While virtual threads present a spectacular modify to how the JVM operates, the code is basically pretty comparable to standard Java threads. The similarity is by style and design and tends to make refactoring present apps and servers comparatively simple. This compatibility also suggests that existing instruments 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


offer com.infoworld

import java.util.Random

general public class App 
  general public static void main( String[] args ) 
    boolean vThreads = args.duration > 
    Procedure.out.println( "Working with vThreads: " + vThreads)

    very long get started = Program.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.

Related Post