Project Loom:Java虚拟线程关闭

几天前,罗恩·普莱斯勒(Ron Pressler)在《织机国》(State of Loom)上生了一篇文章,只有最懒惰的贾维斯特才找不到。这篇文章真的很好,里面有很多有趣的隐喻,我现在将不加提及地使用这些隐喻,而无需参考源代码。

就我而言,我无意中让我表达了一些怀疑,但是当使用Loom Project时,终于可以真正对其进行研究了。一个小时后,从字面上看,罗恩本人收到答复-“你试试!” 好吧,我不得不尝试。

实验需要什么:


首先,我决定看一下Kotlin Coroutine文档中带有流经典示例该示例是用Kotlin编写的,但是用Java重写它并不困难:

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());
    }
}

我们开始并确保该示例仍然挂起,如前所述:

javac Main.java && java Main

现在,使用Project Loom为我们提供的虚拟线程重写示例:

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

结果很快就到了:

1000000

多久?
, .
-, Gil Tene, — .
-, Project Loom, . , — .

但是它本身并没有给我留下太多的印象。最后,使用Kotlin中的协程,可以轻松获得相同的结果。

罗恩(Ron)在其文章中正确地指出,科特林必须引入delay()函数,该协程可以使协程“入睡”,因为Thread.sleep()不会将当前协程发送到睡眠状态,但是当前的调度程序线程并不多,通常按数量计中央处理器

织机计划呢?

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

结果:

- 400K

但这已经很有趣了!使用Project Loom,对Thread.sleep()的调用可以区分它是在常规线程中还是在虚拟线程中,并且工作方式有所不同。

这本身非常酷。但是让我们更深入地研究一下:

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();
}

在这里,我们运行10个慢任务和10个快任务。快的任务比慢的任务快一百万倍,因此可以合理地假设它们早日完成。

但是它不存在:

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


直觉仅在慢任务的数量少于CPU内核的数量时才有效。

原因很简单-目前Project Loom使用常规的ForkJoinPool。结果,尽管文档和想法表明虚拟线程是“抢先的”,但目前它们以“合作”的方式运行。实际上,就像科特林的协程。

应该注意的是,在上述文章中,Ron提到他也像常规线程一样对强制的抢占行为进行了反思。但是到目前为止,这还没有实现,因为尚不清楚当有成千上万的线程时,这种行为如何有用。但是,在Go 1.14中,悄悄引入了强制预占

与Go不同,函数调用不会导致上下文切换。像Kotlin一样,也没有暂停。但是您可以使用Thread.yield()或通过调用任何Java IO函数:例如System.out.println(“”)来完成。

计算很简单-大多数实际程序经常使用阻塞IO。首先是他对Project Loom的使用寻求解决。

一些结论:


我不得不承认,尽管最初抱有怀疑,但Loom项目还是给我留下了深刻的印象。对于普通的Java用户,它承诺使用库或框架,而无需切换到另一种语言,即可实现轻量级多线程。听起来不错。

但是,正如我所期望的那样,这个主要革命将在仍然需要一次又一次地解决并发性问题的图书馆开发人员中进行。现在,随着JDK15的发行,此问题可以转移到JVM,因为它们过去曾将内存(GC)和代码(JIT)优化转移到JVM。如果您喜欢用英语阅读,请

链接到我的原始文章

All Articles