Project Loom: Java virtual threads are close

A few days ago, Ron Pressler gave birth to an article in State of Loom , which only the most lazy javist could not find. The article is really good, there are a lot of interesting metaphors in it, which I am going to unscrupulously use now without reference to the source.

For my part, I inadvertently allowed me to express some skepticism, but when with this Project Loom can finally be really worked on. Literally an hour later, a reply came from Ron himself - “and you try!” . Well, I had to try.

What was required for the experiment:


To begin with, I decided to look at what a classic example with streams from the documentation for Kotlin Coroutine s would look like . The example is written in Kotlin, but rewriting it in Java is not difficult:

public class Main {
    public static void main(String[] args) {
        var c = new AtomicLong();
        for (var i = 0; i < 1_000_000; i++) {
            new Thread(() -> {
                c.incrementAndGet();
            }).start();
        }

        System.out.println(c.get());
    }
}

We start and make sure that the example still hangs, as before:

javac Main.java && java Main

Now rewrite the example using the virtual threads that Project Loom provides us with:

for (var i = 0; i < 1_000_000; i++) {
    Thread.startVirtualThread(() -> {
        c.incrementAndGet();
    });
}

The result is not long in coming:

1000000

How long?
, .
-, Gil Tene, — .
-, Project Loom, . , — .

But in itself it did not impress me too much. In the end, with coroutines in Kotlin, the same result is easily achieved.

Ron in his article correctly observes that Kotlin had to introduce the delay () function, which allows the coroutine to “fall asleep”, because Thread.sleep () does not send the current coroutine to sleep, but the current scheduler thread, which is not many, usually by quantity CPU

What about Project Loom?

for (var i = 0; i < 1_000_000; i++) {
  Thread.startVirtualThread(() -> {
    c.incrementAndGet();
    try {
        Thread.sleep(1_000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
  });
}

Result:

- 400K

But this is already interesting! With Project Loom, the call to Thread.sleep () can distinguish whether it is in a regular thread or in a virtual thread, and it works differently.

This in itself is very cool. But let's dig a little deeper:

var threads = new ArrayList<Thread>();
var cores = 10;
for (var i = 0; i < cores; i++) {
    var t = Thread.startVirtualThread(() -> {
        var bestUUID = "";
        for (var j = 0; j < 1_000_000; j++) {
            var currentUUID = UUID.randomUUID().toString();
            if (currentUUID.compareTo(bestUUID) > 0) {
                bestUUID = currentUUID;
            }
        }
        System.out.println("Best slow UUID is " + bestUUID);
    });
    threads.add(t);
}

for (var i = 0; i < cores; i++) {
    var t = Thread.startVirtualThread(() -> {
        var bestUUID = UUID.randomUUID().toString();
        System.out.println("Best fast UUID is " + bestUUID);
    });
    threads.add(t);
}

for (Thread t : threads) {
    t.join();
}

Here we run 10 slow tasks and 10 fast tasks. Fast tasks are faster than slow ones a million times, so it would be logical to assume that they complete earlier.

But it was not there:

Best slow UUID is fffffde4-8c70-4ce6-97af-6a1779c206e1
Best slow UUID is ffffe33b-f884-4206-8e00-75bd78f6d3bd
Best slow UUID is fffffeb8-e972-4d2e-a1f8-6ff8aa640b70
Best fast UUID is e13a226a-d335-4d4d-81f5-55ddde69e554
Best fast UUID is ec99ed73-23b8-4ab7-b2ff-7942442a13a9
Best fast UUID is c0cbc46d-4a50-433c-95e7-84876a338212
Best fast UUID is c7672507-351f-4968-8cd2-2f74c754485c
Best fast UUID is d5ae642c-51ce-4b47-95db-abb6965d21c2
Best fast UUID is f2f942e3-f475-42b9-8f38-93d89f978578
Best fast UUID is 469691ee-da9c-4886-b26e-dd009c8753b8
Best fast UUID is 0ceb9554-a7e1-4e37-b477-064c1362c76e
Best fast UUID is 1924119e-1b30-4be9-8093-d5302b0eec5f
Best fast UUID is 94fe1afc-60aa-43ce-a294-f70f3011a424
Best slow UUID is fffffc24-28c5-49ac-8e30-091f1f9b2caf
Best slow UUID is fffff303-8ec1-4767-8643-44051b8276ca
Best slow UUID is ffffefcb-614f-48e0-827d-5e7d4dea1467
Best slow UUID is fffffed1-4348-456c-bc1d-b83e37d953df
Best slow UUID is fffff6d6-6250-4dfd-8d8d-10425640cc5a
Best slow UUID is ffffef57-c3c3-46f5-8ac0-6fad83f9d4d6
Best slow UUID is fffff79f-63a6-4cfa-9381-ee8959a8323d


Intuition works only as long as the number of slow tasks is less than the number of cores of your CPU.

The reason is simple - at the moment Project Loom uses the usual ForkJoinPool. As a result, despite the fact that the documentation and the idea indicate that virtual streams are “preemtive”, at the moment they behave in a “cooperative” manner. Like the coroutines in Kotlin, actually.

It should be noted that in the aforementioned article, Ron mentions that he also reflects on forced preemtion behavior, as in normal threads. But so far this has not been implemented, because it is not completely clear how such a behavior is useful when there can be tens of thousands of threads. However, in Go 1.14, forced preemtion was quietly introduced .

A function call, unlike Go, does not result in a context switch. Suspend, as in Kotlin, was not delivered either. But you can do with Thread.yield (), or by calling any Java IO function: System.out.println (""), for example.

The calculation is simple - most real programs use blocking IO quite often. And it is his use of Project Loom that seeks to solve in the first place.

A few conclusions:


I have to admit that despite my initial skepticism, Project Loom impressed me positively. For regular Java users, it promises lightweight multithreading without the need for switching to another language, using a library or framework. Which already sounds good.

But the main revolution, as I expect, this project will be made among library developers who still have to solve the concurrency problem again, and again, and again. Now, with the distribution of JDK15, this problem can be transferred to the JVM, as they used to transfer memory (GC) and code (JIT) optimization to the JVM.

Link to my original article if you prefer to read in English.

All Articles