Java Project Loom – Launching 10 million threads (Part 2)

15 / Mar / 2023 by anil.gola 0 comments

In the previous blog, we discussed a detailed overview of Project Loom. Now it’s time for some code. If you have not read about part 2 of this series, please check it out here:

Let us see how we can create virtual threads in Java.

Thread.ofVirtual().unstarted(() -> System.out.println("Thread is not started"));
 Thread.ofVirtual().start(() -> System.out.println("Thread is not started"));

In the first example, we created a virtual thread but in unstarted mode. In second example, we created a virtual thread in started mode. Simple stuff. There’s nothing fancy about it.

Now let’s run the below program and see the output

public static void main(String[] args) throws InterruptedException {
        var thread = Thread.ofVirtual().unstarted(() -> System.out.println(Thread.currentThread()));
        thread.start();
        thread.join();
    }

Output

VirtualThread[#22]/runnable@ForkJoinPool-1-worker-1

We can see that VirtualThread [#22] has been created and is running the task of printing the line. Also, we can see ForkJoinPool-1-worker-1 literal in the output. This means that our VirtualThread[#22] is running on top of a platform thread with the name ForkJoinPool-1-worker-1. Clearly, virtual threads are running on top of the ForkJoinPool. Is it the same common ForkJoinPool that we use to submit the task? Let’s check this out by running the following program:

public static void main(String[] args) throws InterruptedException {
    ForkJoinPool.commonPool().submit(
          () -> System.out.println(Thread.currentThread())
        );
}

Output

Thread[#22,ForkJoinPool.commonPool-worker-1,5,main]

Clearly, ForkJoinPool-1-worker-1 and ForkJoinPool.commonPool-worker-1 are not same. Virtual threads are running on fork join pool which is different from common fork join pool introduced in java 7.

Let’s see one more interesting thing about virtual thread by running the following program

 public static void main(String[] args) throws InterruptedException {
    Thread.ofVirtual().start(() -> System.out.println("Thread is not started"));
    var threads = IntStream.range(0,10)
      .mapToObj(
          index -> Thread.ofVirtual().unstarted(() -> {
              if(index == 0){
                  System.out.println(Thread.currentThread());
              }
              try {
                  Thread.sleep(10);
              } catch (InterruptedException e) {
                  throw new RuntimeException(e);
              }
              if(index == 0){
                  System.out.println(Thread.currentThread());
              }
          })).toList();
    threads.forEach(Thread::start);
    for(Thread thread : threads){
        thread.join();
    }
  }

We are creating 10 virtual threads and letting the 0th thread print its thread name. We allow it to sleep for 10 milliseconds, and then we are again trying to print the 0th thread to print its name. Let’s see the result.

VirtualThread[#25]/runnable@ForkJoinPool-1-worker-2
VirtualThread[#25]/runnable@ForkJoinPool-1-worker-10

We can see that Virtual Thread #25 first ran on platform thread ForkJoinPool-1-worker-2. It’s been asleep for 10 milliseconds. When it wakes up, it is pinned to another platform thread with the name runnable@ForkJoinPool-1-worker-10. So clearly, we can see that whenever the virtual thread is blocked, it releases the platform thread, and its stack is moved to the heap memory. The platform thread can now be pinned to another virtual thread to carry out its task. When the blocking operation is finished, the virtual thread will be pinned to any free platform thread and will mount its stack on that platform thread.

This is new in Java, and it’s great. Blocking a thread is cheap. Cost is only un-mounting the call-stack from platform thread and storing in heap and vice versa.

Now it’s the time to launch threads. Lets launch 100 of threads with the below program.

 public static void main(String[] args) throws InterruptedException {
     var list = new HashSet<String>();
     var millis = System.currentTimeMillis();
     var vthreadcounts = 100;
     var threads = IntStream.range(0,vthreadcounts)
        .mapToObj(
          index -> Thread.ofVirtual().unstarted(() -> {
              var abc =  "VirtualThread[#22]/runnable@ForkJoinPool-1-worker-1".contains("runnable@ForkJoinPool-1");
              var threadName = Thread.currentThread().toString();
             if(abc) list.add(threadName.substring(threadName.indexOf("/")));
          })).toList();

      threads.forEach(Thread::start);
      for(Thread thread : threads){
          thread.join();
      }
      var millis1 = System.currentTimeMillis();
      System.out.println("millis used to launch " + vthreadcounts + "vthreads:" + (millis1 - millis) + "ms");
      System.out.println("number of platform thread used:" + list.size());
   }

We are using some logic to calculate the number of platform threads required to run 100 virtual threads. Also, we wrote logic to calculate the time it takes in milliseconds to execute it. Let’s see the output of this program.

millis used to launch 100vthreads:23ms
number of platform thread used:7

It took 23 milliseconds and have used 7 platform thread.

Let’s us put more pressure on the program by changing the vthreadcounts to 1000. Let’s see the output now.

millis used to launch 1000vthreads:23ms
number of platform thread used:10

Nothing, its still took 23 milliseconds and platform thread used is 10 in this case. Let us put more pressure on it and run for 10,000 threads and see the output.

millis used to launch 10000vthreads:57ms
number of platform thread used:10

It took now 57 milliseconds and platform thread used is 10. Quite impressive. Let us put some more pressure on it and run for 100,000 threads and see the result.

millis used to launch 100000vthreads:246ms
number of platform thread used:10

Still only 246 milliseconds and 10 platform threads. Still under 1 second. Time to put some serious pressure and look for 1000,000 (1 million) threads.

millis used to launch 1000000vthreads:1286ms
number of platform thread used:10

Now, it took 1286 milliseconds, just little more that 1 second and used 10 platform thread.

Now, we are in finale and will run for 10 million threads i.e 10,000,000 threads. Have you ever done that. This is insane amount of numbers.

millis used to launch 10000000vthreads:8762ms
number of platform thread used:10

And we are able to run our 10 million threads in just less than 9 seconds.

Even though we are not doing much in our threads but still running 10 million threads in less than 9 seconds is no mean feat.

Project loom guys are doing great job. In coming parts we will be taking a look into StructuredTaskScope which is quite amazing feature of this project Loom.

This blog was originally published in Medium, read here.

FOUND THIS USEFUL? SHARE IT

Leave a Reply

Your email address will not be published. Required fields are marked *