Java21-Virtual thread small test-meethigher

Other languages, such as Go, have early supported something called coroutines, which are lightweight threads, while Java asynchronous programming only has the concept of threads. The overall changes brought about by the upgrades after JDK8 are not big, but the Virtual Thread brought by JDK21 this time is still worth experiencing. It can be said to be YYDS. I finally have a reason not to use Java8!

First download JDK 21.

As officially stated, Virtual Thread is still a preview version in JDK19 and JDK20. The debut was officially confirmed in JDK21. Therefore, the existing version can be officially used.

I call all the Virtual Threads below virtual threads, not coroutines, it’s just a name anyway.

However, after the new version is released, if you want to use it officially, you still need to wait for the IDE to be updated, otherwise the user experience will not be so good.

The following tests are all executed through the JDK native compilation command.

1. Quick Start

1.1 How to create

Java’s virtual thread is implemented based on the ForkJoinPool thread pool, which is suitable for intensive blocking scenarios.

Under normal circumstances, if there is blocking, the thread will be stuck there. It will do nothing during this period, but it will occupy the pit. In fact, this part of the time can still be used to do other things, just like netty’s event-driven non-blocking, so virtual threads came into being.

In human terms, virtual threads are suitable for handling a large number of blocking tasks. If you are dealing with computing tasks or a small number of blocking tasks, the advantage is not obvious.

What is obtained by new Thread() in Java corresponds to the thread in the operating system. However, in JDK21, he was given a clearer concept, platform thread PlatformThread.

Don’t ask for a deep understanding, just know how to use it. As for how to create PlatformThread and VirtualThread, please see the following code.

//Thread, that is, platform thread. two ways
Thread platformThread = new Thread(new TestRunner(null));
Thread platformThread1 = Thread.ofPlatform().unstarted(new TestRunner(null));
//Virtual thread. Following the source code, we can see that it relies on the pooled ForkJoinPool.
Thread virtualThread = Thread.ofVirtual().unstarted(new TestRunner(null));

1.2 Performance comparison

The following compares the execution performance of PlatformThread and VirtualThread when handling intensive blocking tasks.

import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;

public class Main {<!-- -->


    /**
     * Virtual thread implemented based on 15 thread pools
     * Execute 10,000 tasks, each task takes 1000 milliseconds, a total of 2637 milliseconds
     */
    public static void virtualThread(int count) throws Exception {<!-- -->
        StopWatcher stopWatcher = new StopWatcher();
        stopWatcher.start();
        CountDownLatch countDownLatch = new CountDownLatch(count);
        for (int i = 0; i < count; i + + ) {<!-- -->
            Thread.ofVirtual().start(new TestRunner(countDownLatch));
        }
        countDownLatch.await();
        stopWatcher.stop();
        System.out.printf("This execution took: %s milliseconds", stopWatcher.getTimeInterval().toMillis());
    }

    /**
     * Based on 15 pooled threads
     * Execute 10,000 tasks, each task takes 1000 milliseconds, a total of 11 minutes
     */
    public static void platformThread(int count) throws Exception {<!-- -->
        StopWatcher stopWatcher = new StopWatcher();
        stopWatcher.start();
        CountDownLatch countDownLatch = new CountDownLatch(count);
        for (int i = 0; i < count; i + + ) {<!-- -->
            CompletableFuture.runAsync(new TestRunner(countDownLatch));
        }
        countDownLatch.await();
        stopWatcher.stop();

        System.out.printf("This execution took: %s milliseconds", stopWatcher.getTimeInterval().toMillis());
    }

    public static void main(String[] args) throws Exception {<!-- -->
        int count = 10000;
        //virtualThread(count);
        platformThread(count);

    }


    public static class TestRunner implements Runnable {<!-- -->

        private final CountDownLatch countDownLatch;

        public TestRunner(CountDownLatch countDownLatch) {<!-- -->
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {<!-- -->
            try {<!-- -->
                System.out.println(Thread.currentThread() + " start " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                Thread.sleep(1000);
                System.out.println(Thread.currentThread() + " stop " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                countDownLatch.countDown();
            } catch (Exception e) {<!-- -->
                System.out.println(e.getMessage());
            }
        }
    }


    public static class StopWatcher {<!-- -->
        private long start;

        private long stop;

        public StopWatcher() {<!-- -->
        }

        public void start() {<!-- -->
            this.start = System.currentTimeMillis();
        }

        public void stop() {<!-- -->
            this.stop = System.currentTimeMillis();
        }

        public Duration getTimeInterval() {<!-- -->
            return Duration.ofMillis(this.stop - this.start);
        }


    }
}

Compile and run.

javac Main.java & amp; & java Main

In the two methods, 10,000 blocking tasks are simulated respectively, and each task is blocked for 1 second.

  • PlatformThread: 15 pooled threads
  • VirtualThread: 15 pooled threads, but using virtual threads

I won’t describe my hardware situation in detail. By directly comparing the results, you can clearly feel the difference.

Results of the

  1. Time-consuming comparison
    • PlatformThread: took 11 minutes
    • VirtualThread: takes 2 seconds
  2. CPU usage comparison
    • PlatformThread: takes up about 10%
    • VirtualThread: takes up about 50%

In summary, to handle intensive blocking tasks, use VirtualThread to maximize CPU performance!

The official has made it clear here that virtual threads are only suitable for intensive blocking scenarios. If it is like a calculation type, it will reduce performance.

To put it bluntly, virtual threads squeeze the idle time of the CPU and do not allow it to be idle. This is similar to the operating system’s time slice and Netty’s event driver.

2. Practical cases

2.1 Shopping

Please see the following code

For example, findUserByName and loadCardFor are queried through the database. In fact, during the query process, the request is sent to the database and the process of waiting for the database response is blocked.

In this case of sequential execution, there is a problem of insufficient CPU utilization, so asynchronous programming can be used to improve performance. But can using multi-threading improve performance?

Let’s analyze the business first. This is a shopping process.

  1. User: Query and obtain users
  2. Shopping cart: Query and obtain the shopping cart through the user, and obtain the total price of the shopping cart
  3. Order: Pay the fee corresponding to the user’s total price and obtain the order
  4. Notification: Notify users of order information

You will find that these are interlocking and there is no parallel business.

Even if we make the code asynchronous as follows, does it make sense? no point!

If 100 requests come at the same time, you will find that the total blocking time has not changed at all. Performance has not improved.

2.1 Shopping-Optimized Version

So how to improve performance? You have to start with blocking and make it non-blocking. In this way, more requests can be processed per unit time.

Moreover, the above asynchronous code form cannot be used, because it is difficult to read and debug.

we hope he

  1. Not blocking
  2. Easy to read and debug

So how to optimize? Please see the following code.

private ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

public void pay(String name) throws Exception {<!-- -->
    executor.submit(() -> {<!-- -->
        User user = userService.findUserByName(name);
        if (!repo.contains(user)) {<!-- -->
            repo.save(user);
        }
        var cart = cartService.loadCartFor(user);
        var total = cart.items().stream().mapToInt(Item::price).sum();
        var transactionId = paymentService.pay(user, total);
        emailService.send(user, cart, transactionId);
    })
}

Using virtual threads is not only easy to debug and read, but also uses the original blocking time to process more requests. **These internal execution processes are handled by Java itself and do not require developers to care. **In the words of foreigners, “This is not magic, this is just engineering.”

If you don’t understand, it is recommended to debug the 1.2 code yourself.

3. Reference acknowledgment

JEP 444: Virtual Threads

Java 21 new feature: Virtual Threads #RoadTo21 – YouTube