天天看點

科技與狠活?JDK19中的虛拟線程到底什麼鬼?

最近,JDK 19釋出了,推出了幾個新的特性,其中有一個比較值得關注的那就是新增了虛拟線程。

科技與狠活?JDK19中的虛拟線程到底什麼鬼?

很多人可能比較疑惑,到底什麼是虛拟線程,和我們現在使用的平台線程有啥差別呢?

要說清楚JDK 19中的虛拟線程,我們要先來了解一下線程都是怎麼實作的。

線程的實作方式

我們都知道,在作業系統中,線程是比程序更輕量級的排程執行機關,線程的引入可以把一個程序的資源配置設定和執行排程分開,各個線程既可以共享程序資源,又可以獨立排程。

其實,線程的實作方式主要有三種:分别是使用核心線程實作、使用使用者線程實作以及使用使用者線程加輕量級程序混合實作。

使用核心線程實作

核心線程(Kernel-Level Thread,KLT)就是直接由作業系統核心(Kernel,下稱核心)支援的線程,這種線程由核心來完成線程切換,核心通過操縱排程器(Scheduler)對線程進行排程,并負責将線程的任務映射到各個處理器上,并向應用程式提供API接口來管理線程。

應用程式一般不會直接去使用核心線程,而是去使用核心線程的一種進階接口——輕量級程序(Light Weight Process,LWP),輕量級程序就是我們通常意義上所講的線程,由于每個輕量級程序都由一個核心線程支援,是以隻有先支援核心線程,才能有輕量級程序。

有了核心線程的支援,每個輕量級程序都成為一個獨立的排程單元,即使有一個輕量級程序在系統調用中阻塞了,也不會影響整個程序繼續工作。

但是輕量級程序具有它的局限性:首先,由于是基于核心線程實作的,是以各種線程操作,如建立、析構及同步,都需要進行系統調用。而系統調用的代價相對較高,需要在使用者态(User Mode)和核心态(Kernel Mode)中來回切換。其次,每個輕量級程序都需要有一個核心線程的支援,是以輕量級程序要消耗一定的核心資源(如核心線程的棧空間),是以一個系統支援輕量級程序的數量是有限的。

使用使用者線程實作

在使用者空間建立線程庫,通過運作時系統(Run-time System)來完成線程的管理,因為這種線程的實作是在使用者空間的,是以作業系統的核心并不知道線程的存在,是以核心管理的還是程序,是以這種線程的切換不需要核心操作。

這種實作方式下,一個程序和線程之間的關系是一對多的。

這種線程實作方式的優點是線程切換快,并且可以運作在任何作業系統之上,隻需要實作線程庫就行了。但是缺點也比較明顯,就是所有線程的操作都需要使用者程式自己處理,并且因為大多數系統調用都是阻塞的,是以一旦一個程序阻塞了,那麼程序中的所有線程也會被阻塞。還有就是多處理器系統中如何将線程映射到其他處理器上也是一個比較大的問題。

使用使用者線程加輕量級程序混合實作

還有一種混合實作的方式,就是線程的建立在使用者空間完成,通過線程庫進行,但是線程的排程是由核心來完成的。多個使用者線程通過多路複用來複用多個核心線程。這個就不展開講了

Java線程的實作方式

以上講的是作業系統的線程的實作的三種方式,不同的作業系統在實作線程的時候會采用不同的機制,比如windows采用的是核心線程實作的,而Solaris則是通過混合模式實作的。

而Java作為一門跨平台的程式設計語言,實際上他的線程的實作其實是依賴具體的作業系統的。而比較常用的windows和linux來說,都是采用核心線程的方式實作的。

也就是說,當我們在JAVA代碼中建立一個Tread的時候,其實是需要映射到作業系統的線程的具體實作的,因為常見的通過核心線程實作的方式在建立、排程時都需要進行核心參與,是以成本比較高,盡管JAVA中提供了線程池的方式來避免重複建立線程,但是依舊有很大的優化空間。而且這種實作方式意味着受機器資源的影響,平台線程數也是有限制的。

虛拟線程

JDK 19引入的虛拟線程,是JDK 實作的輕量級線程,他可以避免上下文切換帶來的的額外耗費。他的實作原理其實是JDK不再是每一個線程都一對一的對應一個作業系統的線程了,而是會将多個虛拟線程映射到少量作業系統線程中,通過有效的排程來避免那些上下文切換。

科技與狠活?JDK19中的虛拟線程到底什麼鬼?

而且,我們可以在應用程式中建立非常多的虛拟線程,而不依賴于平台線程的數量。這些虛拟線程是由JVM管理的,是以它們不會增加額外的上下文切換開銷,因為它們作為普通Java對象存儲在RAM中。

虛拟線程與平台線程的差別

首先,虛拟線程總是守護線程。setDaemon (false)方法不能将虛拟線程更改為非守護線程。是以,需要注意的是,當所有啟動的非守護程序線程都終止時,JVM将終止。這意味着JVM不會等待虛拟線程完成後才退出。

其次,即使使用setPriority()方法,虛拟線程始終具有normal的優先級,且不能更改優先級。在虛拟線程上調用此方法沒有效果。

還有就是,虛拟線程是不支援stop()、suspend()或resume()等方法。這些方法在虛拟線程上調用時會抛出UnsupportedOperationException異常。

如何使用虛拟線程

接下來介紹一下,在JDK 19中如何使用虛拟線程。

首先,通過Thread.startVirtualThread()可以運作一個虛拟線程:

Thread.startVirtualThread(() -> {
    System.out.println("虛拟線程執行中...");
});           

其次,通過Thread.Builder也可以建立虛拟線程,Thread類提供了ofPlatform()來建立一個平台線程、ofVirtual()來建立虛拟現場。

Thread.Builder platformBuilder = Thread.ofPlatform().name("平台線程");
Thread.Builder virtualBuilder = Thread.ofVirtual().name("虛拟線程");


Thread t1 = platformBuilder .start(() -> {...}); 
Thread t2 = virtualBuilder.start(() -> {...});           

另外,線程池也支援了虛拟線程,可以通過Executors.newVirtualThreadPerTaskExecutor()來建立虛拟線程:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        });
    });
}           

但是,其實并不建議虛拟線程和線程池一起使用,因為Java線程池的設計是為了避免建立新的作業系統線程的開銷,但是建立虛拟線程的開銷并不大,是以其實沒必要放到線程池中。

性能差異

說了半天,虛拟線程到底能不能提升性能,能提升多少呢?我們來做個測試。

我們寫一個簡單的任務,在控制台中列印消息之前等待1秒:

final AtomicInteger atomicInteger = new AtomicInteger();


Runnable runnable = () -> {
  try {
    Thread.sleep(Duration.ofSeconds(1));
  } catch(Exception e) {
      System.out.println(e);
  }
  System.out.println("Work Done - " + atomicInteger.incrementAndGet());
};           

現在,我們将從這個Runnable建立10,000個線程,并使用虛拟線程和平台線程執行它們,以比較兩者的性能。

先來我們比較熟悉的平台線程的實作:

Instant start = Instant.now();


try (var executor = Executors.newFixedThreadPool(100)) {
  for(int i = 0; i < 10_000; i++) {
    executor.submit(runnable);
  }
}


Instant finish = Instant.now();
long timeElapsed = Duration.between(start, finish).toMillis();  
System.out.println("總耗時 : " + timeElapsed);           

輸出結果為:

總耗時 : 102323           

總耗時大概100秒左右。接下來再用虛拟線程跑一下看看

因為在JDK 19中,虛拟線程是一個預覽API,預設是禁用。是以需要使用$ java——source 19——enable-preview xx.java 的方式來運作代碼。
Instant start = Instant.now();


try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
  for(int i = 0; i < 10_000; i++) {
    executor.submit(runnable);
  }
}


Instant finish = Instant.now();
long timeElapsed = Duration.between(start, finish).toMillis();  
System.out.println("總耗時 : " + timeElapsed);           

使用 Executors.newVirtualThreadPerTaskExecutor()來建立虛拟線程,執行結果如下:

總耗時 : 1674           

總耗時大概1.6秒左右。

100秒和1.6秒的差距,足以看出虛拟線程的性能提升還是立竿見影的。

總結

本文給大家介紹了一下JDK 19新推出的虛拟線程,或者叫協程,主要是為了解決在讀書作業系統中線程需要依賴核心線程的實作,導緻有很多額外開銷的問題。通過在Java語言層面引入虛拟線程,通過JVM進行排程管理,進而減少上下文切換的成本。

同時我們經過簡單的demo測試,發現虛拟線程的執行确實高效了很多。但是使用的時候也需要注意,虛拟線程是守護線程,是以有可能會沒等他執行完虛拟機就會shutdown掉。