Java 虛擬線程簡介

前言

Java傳統(tǒng)的線程和操作系統(tǒng)中的線程是一一對應(yīng)關(guān)系,意味著創(chuàng)建一個Java線程的同時會創(chuàng)建出一個操作系統(tǒng)線程。這樣會帶來如下問題:

  • 操作系統(tǒng)線程的創(chuàng)建代價很高,需要分配堆棧和消耗大量時間。
  • 操作系統(tǒng)線程的上下文切換需要大量的CPU操作,代價高昂。
  • 創(chuàng)建大量的系統(tǒng)線程對操作系統(tǒng)的壓力極大,會影響系統(tǒng)的響應(yīng)速度和穩(wěn)定性。
  • 在傳統(tǒng)Java線程中大量執(zhí)行阻塞操作會嚴(yán)重浪費(fèi)系統(tǒng)線程。CPU多核心性能無法充分利用。不得不使用異步非阻塞編程。異步編程寫法遠(yuǎn)比同步復(fù)雜。
  • 為了避免創(chuàng)建和銷毀大量線程,必須使用線程池化技術(shù)。

從Java 19開始引入,在Java 21 GA的虛擬線程解決了傳統(tǒng)線程代價過大的問題,能夠創(chuàng)建出很多的虛擬線程而無需擔(dān)心資源占用。用戶可以像使用傳統(tǒng)線程一樣去使用虛擬線程(用戶無感知)。

虛擬線程和傳統(tǒng)線程最大的區(qū)別是:操作系統(tǒng)線程和Java傳統(tǒng)線程是一一對應(yīng)的關(guān)系,但操作系統(tǒng)線程和虛擬線程是一對多的關(guān)系。

虛擬線程介紹

虛擬線程并非真正的線程。虛擬線程也是基于Thread實(shí)現(xiàn),使用的方式和行為于傳統(tǒng)Java線程完全一樣。但是虛擬線程在執(zhí)行的時候離不開系統(tǒng)線程。執(zhí)行虛擬線程的系統(tǒng)線程稱之為Carrier載體線程。

虛擬線程有如下概念:

  • Carrier(載體)線程:在platform(平臺/系統(tǒng))運(yùn)行的線程,JVM把n個虛擬線程映射為m個carrier線程。
  • Mount(掛載)和Unmount(卸載):把虛擬線程切換到carrier線程運(yùn)行稱為mount。虛擬線程停止執(zhí)行稱之為unmount。

虛擬線程的掛載和卸載操作由JVM內(nèi)部的調(diào)度器實(shí)現(xiàn),用戶無需直接干涉。

虛擬線程在開始運(yùn)行的時候,會被臨時掛載到載體線程上。如果遇到下面的情況之一,會被卸載并讓出載體線程:

  • 文件/網(wǎng)絡(luò)IO阻塞
  • 使用Concurrent庫引起等待
  • Thread.sleep()

卸載之后載體線程可以用來執(zhí)行其他的虛擬線程。

載體線程使用池化方式管理,線程池為ForkJoinPool。源代碼參見VirtualThread::createDefaultScheduler。

ForkJoinPool的配置有如下3個系統(tǒng)參數(shù):

  • jdk.virtualThreadScheduler.parallelism:并行度,決定WorkQueue大小。默認(rèn)是Runtime.availableProcessors(CPU數(shù)量)。
  • jdk.virtualThreadScheduler.maxPoolSize:線程池最大線程數(shù)。
  • jdk.virtualThreadScheduler.minRunnable:至少可運(yùn)行的線程數(shù)。

使用方式

  1. 使用類似于傳統(tǒng)線程new Thread()的方式,使用Thread類的ofVirtual()方法,創(chuàng)建出一個虛擬線程。
private static Thread startVirtualThread(String name, Runnable runnable) {
    return Thread.ofVirtual().name(name).start(runnable);
}
  1. 使用類似于線程池的方式。通過Executors來創(chuàng)建虛擬線程。每提交一個任務(wù)就創(chuàng)建出一個虛擬線程。
try (var executorService = Executors.newVirtualThreadPerTaskExecutor()) {
    executorService.submit(...);
}
  1. 使用指定的線程工廠類創(chuàng)建虛擬線程。
final ThreadFactory factory = Thread.ofVirtual().name("virtualThread-", 0).factory();
try (var executor = Executors.newThreadPerTaskExecutor(factory)) {
    // ...
}

打印Virtual Thread及其掛載的載體線程名稱,可以使用System.out.println(Thread.currentThread())方式。

通過Thread.currentThread().isVirtual()來判斷當(dāng)前代碼是否在虛擬線程中運(yùn)行。

使用建議

  • 虛擬線程適用于大量阻塞IO的場景。不適用于CPU密集型負(fù)載。虛擬線程并非執(zhí)行更快的線程。
  • 在虛擬線程中編寫同步阻塞請求代碼。使用每次請求創(chuàng)建一個虛擬線程的方式。
  • 為每個并發(fā)任務(wù)創(chuàng)建一個虛擬線程,不要在虛擬線程上使用線程池。
  • 使用Semaphore限制并發(fā)度。
  • 不要在虛擬線程中緩存大量可重用對象。
  • 避免虛擬線程頻繁和長時間掛起(pinned)的情況。

官網(wǎng)對掛起的情況有說明:

A virtual thread cannot be unmounted during blocking operations when it is pinned to its carrier. A virtual thread is pinned in the following situations:

  • The virtual thread runs code inside a synchronized block or method
  • The virtual thread runs a native method or a foreign function (see Foreign Function and Memory API)

可以使用-Djdk.tracePinnedThreads=full/short跟蹤pinned的虛擬線程。

接下來舉個例子。該例子中的SynchronizedWorkloadwork方法使用synchronized關(guān)鍵字。會造成虛擬線程長時間pin在載體線程上。ReentrantLockWorkload使用ReentrantLock不會有這個問題。

package org.example;

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.locks.ReentrantLock;

public class VirtualThreadDemo {
    public static void main(String[] args) {
//      這里將載體線程數(shù)限制為1,方便演示
        System.setProperty("jdk.virtualThreadScheduler.parallelism", "1");
        System.setProperty("jdk.virtualThreadScheduler.maxPoolSize", "1");
        System.setProperty("jdk.virtualThreadScheduler.minRunnable", "1");
        System.setProperty("jdk.tracePinnedThreads", "full");
        var lockWorkload = new ReentrantLockWorkload();
//        var lockWorkload = new SynchronizedWorkload();
        var workload = new Workload();
        ThreadFactory threadFactory = Thread.ofVirtual().name("workload", 0).factory();
        try (var executorService = Executors.newThreadPerTaskExecutor(threadFactory)) {
            executorService.submit(lockWorkload::work);
            executorService.submit(workload::work);
        }
    }

    static class Workload {
        public void work() {
            try {
                System.out.println("Workload Started");
                System.out.println(Thread.currentThread());
                Thread.sleep(1000);
                System.out.println("Workload Finished");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static class SynchronizedWorkload {
        public synchronized void work() {
            try {
                System.out.println("SynchronizedWorkload Started");
                System.out.println(Thread.currentThread());
                Thread.sleep(1000);
                System.out.println("SynchronizedWorkload Finished");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static class ReentrantLockWorkload {
        private static final ReentrantLock lock = new ReentrantLock();

        public void work() {
            try {
                lock.lock();
                System.out.println("ReentrantLockWorkload Started");
                System.out.println(Thread.currentThread());
                Thread.sleep(1000);
                System.out.println("ReentrantLockWorkload Finished");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

例子中lockWorkloadnew SynchronizedWorkload()的時候,執(zhí)行輸出如下:

SynchronizedWorkload Started
VirtualThread[#22,workload0]/runnable@ForkJoinPool-1-worker-1
Thread[#23,ForkJoinPool-1-worker-1,5,CarrierThreads]
    java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:183)
    java.base/jdk.internal.vm.Continuation.onPinned0(Continuation.java:393)
    java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:621)
    java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:793)
    java.base/java.lang.Thread.sleep(Thread.java:507)
    org.example.VirtualThreadDemo$SynchronizedWorkload.work(VirtualThreadDemo2.java:42) <== monitors:1
    java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
    java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
    java.base/java.lang.VirtualThread.run(VirtualThread.java:309)
SynchronizedWorkload Finished
Workload Started
VirtualThread[#24,workload1]/runnable@ForkJoinPool-1-worker-1
Workload Finished

我們發(fā)現(xiàn)SynchronizedWorkloadWorkload實(shí)際上時串行執(zhí)行。當(dāng)虛擬線程執(zhí)行進(jìn)入synchronized代碼塊的時候會pin到載體線程上,無法卸載。即便是代碼塊中有sleep或者阻塞IO也不會卸載。在上面的執(zhí)行結(jié)果中還能夠看到JVM跟蹤到了pinned的虛擬線程日志記錄。

修改一下代碼,例子中lockWorkloadnew ReentrantLockWorkload()的時候,執(zhí)行輸出如下:

ReentrantLockWorkload Started
VirtualThread[#22,workload0]/runnable@ForkJoinPool-1-worker-1
Workload Started
VirtualThread[#24,workload1]/runnable@ForkJoinPool-1-worker-1
ReentrantLockWorkload Finished
Workload Finished

這是我們期待的結(jié)果,ReentrantLockWorkloadWorkload可以并行執(zhí)行。當(dāng)ReentrantLockWorkload加鎖,進(jìn)入sleep狀態(tài)的時候仍然可以卸載,讓出載體線程,從而Workload才有能夠有機(jī)會執(zhí)行。

參考文獻(xiàn)

https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html

https://blog.moyucoding.com/jvm/2023/09/23/ultimate-guide-to-java-virtual-thread

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容