JEP 嚐鮮系列 3 - 使用虛執行緒進行同步網路 IO 的不阻塞原理

乾貨滿滿張雜湊發表於2021-05-25

相關 JEP:

使用虛執行緒進行網路 IO

Project Loom 主要目標是在 Java 平臺上提供一種易於使用、高吞吐量的輕量級併發性和新的程式設計模型的 JVM 特性和API。這帶來了許多有趣和令人興奮的前景,其中之一是簡化網路互動的程式碼的同時兼顧效能。現在的伺服器能夠處理開啟的套接字連線的數量,遠遠超過它們能夠支援的執行緒數量,這既帶來了機遇,也帶來了挑戰。

但是不幸的是,編寫與網路互動的可伸縮程式碼是很困難的。我們一般使用同步 API 的方式進行編碼,但是在超過一定閾值之後,同步程式碼就迎來了瓶頸,很難進行伸縮。因為這樣的API在執行 I/O 操作時會阻塞,而 I/O 操作又會將執行緒繫結起來,直到操作就緒,例如嘗試從套接字讀取資料但是當前並沒有資料要讀取的時候。目前的執行緒,在 Java 平臺中是一個昂貴的資源,以至於無法等待 I/O 操作的完成再去釋放。為了解決這個限制,我們通常使用非同步 I/O 或 Ractor 框架,因為它們可以構造出在 I/O 操作中不用繫結執行緒的程式碼,而是在 I/O 操作完成或準備就緒時使用回撥或事件通知執行緒進行處理。

使用非同步和非阻塞 API 比使用同步 API 更具有挑戰性,部分原因是用這些 API 寫出來的程式碼是比較反人類的。同步API在很大程度上更容易使用;程式碼更易於編寫、更容易閱讀和更易於除錯,除錯的時候堆疊裡面的資訊大部分是有用的。但是如前所述,使用同步 API 的程式碼不能像非同步程式碼那樣伸縮擴充套件,因此我們必須做一個艱難的選擇:選擇更簡單的同步程式碼,並接受它不會擴充套件;或者選擇更可伸縮的非同步程式碼,並處理所有的複雜性。兩個都不是個好選擇!Project Loom 主要就是要讓同步程式碼也能靈活伸縮擴充套件。

在本文中,我們將檢視 Java 平臺的網路 API 在虛擬執行緒上被呼叫時是如何工作的。瞭解底層細節,我們才能更好地、更放心地使用虛擬執行緒(纖程)。

虛擬執行緒(纖程

在進一步研究之前,我們需要了解一下ProjectLoom中的新執行緒--虛擬執行緒(也可以稱為纖程)。

虛擬執行緒是使用者態執行緒,被 JVM 管理,而不是作業系統。虛擬執行緒佔用的系統資源很少,一個 JVM 可以容納百萬量級的虛擬執行緒。特別適合於經常執行阻塞時間比較長,經常等待 IO 的任務。

平臺執行緒(即目前 Java 平臺的執行緒),是和作業系統核心執行緒一一對應的。平臺執行緒通常擁有一個非常大的棧,以及其他的一些系統維護的資源。虛擬執行緒則使用一小組用作載體執行緒的平臺執行緒。在虛擬執行緒中執行的程式碼通常不會知道底層承載的執行緒。鎖和 I/O 操作是將承載執行緒從一個虛擬執行緒重新排程到另一個虛擬執行緒的排程點。虛擬執行緒可能會 parked(例如LockSupport.park()),從而使其無法排程。一個已 parked 的虛擬執行緒可能被取消(例如LockSupport.unpark(Thread)),這樣重新啟用了它的排程。

網路 API

Java 平臺中主要有兩種網路 API:

  1. 非同步 - AsynchronousServerSocketChannelAsynchronousSocketChannel
  2. 同步 - java.net.Socketjava.net.ServerSocketjava.net.DatagramSocketjava.nio.channels.SocketChanneljava.nio.channels.ServerSocketChanneljava.nio.channels.DatagramChannel

第一類非同步 API,建立啟動在之後某個時間完成的 I/O 操作,可能在啟動 I/O 操作的執行緒之外的執行緒上完成。根據定義,這些 API 不會導致阻塞的系統呼叫,因此在虛擬執行緒中執行時不需要特殊處理

第二類同步 API,從它們在虛擬執行緒中執行時的行為角度來看,它們更有趣。在這些 API 中,NIO channel 相關的可以配置成為非阻塞模式。這種 channel 通常使用 I/O 事件通知機制實現,例如註冊到 Selector 上監聽事件。類似於非同步網路 API,在虛擬執行緒中執行不需要額外處理,因為 I/O 操作不自己呼叫阻塞的系統呼叫,這個呼叫留給了 Selector。最後,我們來看看將 channel 配置成為阻塞模式以及 java.net 相關 API 的情況(我們這裡稱這種 API 為同步阻塞 API)。同步 API 的語義要求 I/O 操作一旦啟動,在呼叫執行緒中完成或失敗,然後將控制權返回給呼叫方。但是,如果 I/O 操作“尚未準備好”怎麼辦呢?例如,目前沒有資料可以讀取。

同步阻塞 API

在虛擬執行緒中執行的 Java 同步網路 API 會將底層原生 Socket 切換到非阻塞模式。當 Java 程式碼啟用一個 I/O 請求並且這個請求沒有立即完成(原生 socket 返回 EAGAIN - 代表"未就緒"/"會阻塞")的時候,這個底層 socket 會被註冊到一個 JVM 內部事件通知機制(Poller),並且虛擬執行緒會被 parked。當底層 I/O 操作就緒的時候(有相關事件會到達 Poller),虛擬執行緒會 unparked 並且底層的 Socket 操作會被重試處理。

我們來用一個例子仔細看下這其中的原理,首先,我們需要下載 project loom 的 JDK(地址:http://jdk.java.net/loom/),並解壓使用。

接下來編寫程式碼:

//Java 16 中的 Record 物件,可以理解為有包含兩個 final 屬性(url 和 response)的類
static record URLData (URL url, byte[] response) { }

static List<URLData> retrieveURLs(URL... urls) throws Exception {
  //建立虛擬執行緒執行緒池
  try (var executor = Executors.newVirtualThreadExecutor()) {
    //生成讀取對每個 url 執行 getURL 方法的任務
    var tasks = Arrays.stream(urls)
            .map(url -> (Callable<URLData>)() -> getURL(url))
            .toList();
    //提交任務,等待並返回所有結果
    return executor.submit(tasks)
            .filter(Future::isCompletedNormally)
            .map(Future::join)
            .toList();
  }
}

//讀取url的內容
static URLData getURL(URL url) throws IOException {
  try (InputStream in = url.openStream()) {
    return new URLData(url, in.readAllBytes());
  }
}

public static void main(String[] args) throws Exception {
    //訪問 google,由於你懂得,會比較慢
    List<URLData> urlData = retrieveURLs(new URL("https://www.google.com/"));
}

我們使用 retrieveURLs 訪問谷歌,肯定會很慢,來保證能採集到堆疊。同時,不能用 jstack 採集堆疊(目前 jstack 採集不到虛擬執行緒堆疊,只能採集到承載執行緒的堆疊),需要用 jcmd 命令中的 JavaThread.dump 採集。同時,為了能採集到我們想要的堆疊,我們需要一些小操作。

首先,我們在 getURL(URL url) 方法的第一行打斷點,debug 到這裡暫停。然後執行命令:

> jps
25496 LoomThreadMain
12512 Jps

> jcmd 25496 JavaThread.dump threads.txt -overwrite

然後繼續執行程式,再執行命令,採集虛擬執行緒執行 I/O 操作時候的堆疊:

> jcmd 25496 JavaThread.dump threads2.txt -overwrite

我們檢視threads.txt這個檔案,其中我們關心的執行緒資訊是:

"main" #1
      java.base@17-loom/jdk.internal.misc.Unsafe.park(Native Method)
      java.base@17-loom/java.util.concurrent.locks.LockSupport.park(LockSupport.java:371)
      java.base@17-loom/java.util.concurrent.LinkedTransferQueue$Node.block(LinkedTransferQueue.java:470)
      java.base@17-loom/java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3470)
      java.base@17-loom/java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3441)
      java.base@17-loom/java.util.concurrent.LinkedTransferQueue.awaitMatch(LinkedTransferQueue.java:669)
      java.base@17-loom/java.util.concurrent.LinkedTransferQueue.xfer(LinkedTransferQueue.java:616)
      java.base@17-loom/java.util.concurrent.LinkedTransferQueue.take(LinkedTransferQueue.java:1286)
      java.base@17-loom/java.util.concurrent.ExecutorServiceHelper$BlockingQueueSpliterator.tryAdvance(ExecutorServiceHelper.java:197)
      java.base@17-loom/java.util.Spliterator.forEachRemaining(Spliterator.java:326)
      java.base@17-loom/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
      java.base@17-loom/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
      java.base@17-loom/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:550)
      java.base@17-loom/java.util.stream.AbstractPipeline.evaluateToArrayNode(AbstractPipeline.java:260)
      java.base@17-loom/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:616)
      java.base@17-loom/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:622)
      java.base@17-loom/java.util.stream.ReferencePipeline.toList(ReferencePipeline.java:627)
      app//com.github.hashjang.LoomThreadMain.retrieveURLs(LoomThreadMain.java:43)
      app//com.github.hashjang.LoomThreadMain.main(LoomThreadMain.java:29)

"ForkJoinPool-1-worker-1" #27
      java.base@17-loom/java.lang.Continuation.run(Continuation.java:300)
      java.base@17-loom/java.lang.VirtualThread.runContinuation(VirtualThread.java:240)
      java.base@17-loom/java.lang.VirtualThread$$Lambda$25/0x0000000801053fc0.run(Unknown Source)
      java.base@17-loom/java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1395)
      java.base@17-loom/java.util.concurrent.ForkJoinTask.doExec$$$capture(ForkJoinTask.java:373)
      java.base@17-loom/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java)
      java.base@17-loom/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1177)
      java.base@17-loom/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1648)
      java.base@17-loom/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1615)
      java.base@17-loom/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165)
      
"<unnamed>" #26 virtual
      java.base/java.util.concurrent.ConcurrentHashMap.transfer(ConcurrentHashMap.java:2431)
      java.base/java.util.concurrent.ConcurrentHashMap.addCount(ConcurrentHashMap.java:2354)
      java.base/java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1075)
      java.base/java.util.concurrent.ConcurrentHashMap.putIfAbsent(ConcurrentHashMap.java:1541)
      java.base/sun.util.locale.LocaleObjectCache.get(LocaleObjectCache.java:68)
      java.base/java.util.Locale.getInstance(Locale.java:841)
      java.base/java.util.Locale.forLanguageTag(Locale.java:1736)
      java.base/sun.util.locale.provider.LocaleProviderAdapter.toLocaleArray(LocaleProviderAdapter.java:323)
      java.base/sun.util.locale.provider.CalendarDataProviderImpl.getAvailableLocales(CalendarDataProviderImpl.java:63)
      java.base/java.util.spi.LocaleServiceProvider.isSupportedLocale(LocaleServiceProvider.java:217)
      java.base/sun.util.locale.provider.LocaleServiceProviderPool.findProviders(LocaleServiceProviderPool.java:306)
      java.base/sun.util.locale.provider.LocaleServiceProviderPool.getLocalizedObjectImpl(LocaleServiceProviderPool.java:274)
      java.base/sun.util.locale.provider.LocaleServiceProviderPool.getLocalizedObject(LocaleServiceProviderPool.java:256)
      java.base/sun.util.locale.provider.CalendarDataUtility.retrieveFirstDayOfWeek(CalendarDataUtility.java:76)
      java.base/java.util.Calendar.setWeekCountData(Calendar.java:3419)
      java.base/java.util.Calendar.<init>(Calendar.java:1612)
      java.base/java.util.GregorianCalendar.<init>(GregorianCalendar.java:738)
      java.base/java.util.Calendar$Builder.build(Calendar.java:1494)
      java.base/sun.util.locale.provider.CalendarProviderImpl.getInstance(CalendarProviderImpl.java:87)
      java.base/java.util.Calendar.createCalendar(Calendar.java:1697)
      java.base/java.util.Calendar.getInstance(Calendar.java:1661)
      java.base/java.text.SimpleDateFormat.initializeCalendar(SimpleDateFormat.java:680)
      java.base/java.text.SimpleDateFormat.<init>(SimpleDateFormat.java:624)
      java.base/java.text.SimpleDateFormat.<init>(SimpleDateFormat.java:603)
      java.base/sun.security.util.DisabledAlgorithmConstraints$DenyAfterConstraint.<clinit>(DisabledAlgorithmConstraints.java:695)
      java.base/sun.security.util.DisabledAlgorithmConstraints$Constraints.<init>(DisabledAlgorithmConstraints.java:424)
      java.base/sun.security.util.DisabledAlgorithmConstraints.<init>(DisabledAlgorithmConstraints.java:149)
      java.base/sun.security.ssl.SSLAlgorithmConstraints.<clinit>(SSLAlgorithmConstraints.java:49)
      java.base/sun.security.ssl.ProtocolVersion.<init>(ProtocolVersion.java:158)
      java.base/sun.security.ssl.ProtocolVersion.<clinit>(ProtocolVersion.java:41)
      java.base/sun.security.ssl.SSLContextImpl$AbstractTLSContext.<clinit>(SSLContextImpl.java:539)
      java.base/java.lang.Class.forName0(Native Method)
      java.base/java.lang.Class.forName(Class.java:375)
      java.base/java.security.Provider$Service.getImplClass(Provider.java:1937)
      java.base/java.security.Provider$Service.getDefaultConstructor(Provider.java:1968)
      java.base/java.security.Provider$Service.newInstanceOf(Provider.java:1882)
      java.base/java.security.Provider$Service.newInstanceUtil(Provider.java:1890)
      java.base/java.security.Provider$Service.newInstance(Provider.java:1865)
      java.base/sun.security.jca.GetInstance.getInstance(GetInstance.java:236)
      java.base/sun.security.jca.GetInstance.getInstance(GetInstance.java:164)
      java.base/javax.net.ssl.SSLContext.getInstance(SSLContext.java:184)
      java.base/javax.net.ssl.SSLContext.getDefault(SSLContext.java:110)
      java.base/javax.net.ssl.SSLSocketFactory.getDefault(SSLSocketFactory.java:83)
      java.base/javax.net.ssl.HttpsURLConnection.getDefaultSSLSocketFactory(HttpsURLConnection.java:334)
      java.base/javax.net.ssl.HttpsURLConnection.<init>(HttpsURLConnection.java:291)
      java.base/sun.net.www.protocol.https.HttpsURLConnectionImpl.<init>(HttpsURLConnectionImpl.java:81)
      java.base/sun.net.www.protocol.https.Handler.openConnection(Handler.java:62)
      java.base/sun.net.www.protocol.https.Handler.openConnection(Handler.java:57)
      java.base/java.net.URL.openConnection(URL.java:1093)
      java.base/java.net.URL.openStream(URL.java:1159)
      com.github.hashjang.LoomThreadMain.getURL(LoomThreadMain.java:48)
      com.github.hashjang.LoomThreadMain.lambda$retrieveURLs$0(LoomThreadMain.java:38)
      java.base/java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:295)
      java.base/java.util.concurrent.FutureTask.run(FutureTask.java)
      java.base/java.util.concurrent.ThreadExecutor$TaskRunner.run(ThreadExecutor.java:385)
      java.base/java.lang.VirtualThread.run(VirtualThread.java:295)
      java.base/java.lang.VirtualThread$VThreadContinuation.lambda$new$0(VirtualThread.java:172)
      java.base/java.lang.Continuation.enter0(Continuation.java:372)
      java.base/java.lang.Continuation.enter(Continuation.java:365)

其中 "<unnamed>" #26 virtual 是我們程式中建立的虛擬執行緒,並且通過堆疊中可以看出,虛擬執行緒還沒有處於 I/O 操作。通過執行緒堆疊也可以看出,這個虛擬執行緒的承載執行緒是 "ForkJoinPool-1-worker-1" #27. 可以看出虛擬執行緒預設的承載執行緒是 Java 8 之後預設會啟動的 common ForkJoinPool 中的執行緒。並且是通過 Continuation 這個類執行虛擬執行緒的工作的。

檢視threads2.txt

"main" #1
      java.base@17-loom/jdk.internal.misc.Unsafe.park(Native Method)
      java.base@17-loom/java.util.concurrent.locks.LockSupport.park(LockSupport.java:371)
      java.base@17-loom/java.util.concurrent.LinkedTransferQueue$Node.block(LinkedTransferQueue.java:470)
      java.base@17-loom/java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3470)
      java.base@17-loom/java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3441)
      java.base@17-loom/java.util.concurrent.LinkedTransferQueue.awaitMatch(LinkedTransferQueue.java:669)
      java.base@17-loom/java.util.concurrent.LinkedTransferQueue.xfer(LinkedTransferQueue.java:616)
      java.base@17-loom/java.util.concurrent.LinkedTransferQueue.take(LinkedTransferQueue.java:1286)
      java.base@17-loom/java.util.concurrent.ExecutorServiceHelper$BlockingQueueSpliterator.tryAdvance(ExecutorServiceHelper.java:197)
      java.base@17-loom/java.util.Spliterator.forEachRemaining(Spliterator.java:326)
      java.base@17-loom/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
      java.base@17-loom/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
      java.base@17-loom/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:550)
      java.base@17-loom/java.util.stream.AbstractPipeline.evaluateToArrayNode(AbstractPipeline.java:260)
      java.base@17-loom/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:616)
      java.base@17-loom/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:622)
      java.base@17-loom/java.util.stream.ReferencePipeline.toList(ReferencePipeline.java:627)
      app//com.github.hashjang.LoomThreadMain.retrieveURLs(LoomThreadMain.java:43)
      app//com.github.hashjang.LoomThreadMain.main(LoomThreadMain.java:29)
      
"ForkJoinPool-1-worker-1" #25
      java.base@17-loom/jdk.internal.misc.Unsafe.park(Native Method)
      java.base@17-loom/java.util.concurrent.locks.LockSupport.parkUntil(LockSupport.java:449)
      java.base@17-loom/java.util.concurrent.ForkJoinPool.awaitWork(ForkJoinPool.java:1719)
      java.base@17-loom/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1616)
      java.base@17-loom/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165)

"Read-Poller" #41
      java.base@17-loom/sun.nio.ch.WEPoll.wait(Native Method)
      java.base@17-loom/sun.nio.ch.WEPollPoller.poll(WEPollPoller.java:64)
      java.base@17-loom/sun.nio.ch.Poller.poll(Poller.java:196)
      java.base@17-loom/sun.nio.ch.Poller.lambda$startPollerThread$0(Poller.java:66)
      java.base@17-loom/sun.nio.ch.Poller$$Lambda$89/0x00000008010e5168.run(Unknown Source)
      java.base@17-loom/java.lang.Thread.run(Thread.java:1521)
      java.base@17-loom/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:161)
      
"<unnamed>" #24 virtual
      java.base/java.lang.Continuation.yield(Continuation.java:402)
      java.base/java.lang.VirtualThread.yieldContinuation(VirtualThread.java:367)
      java.base/java.lang.VirtualThread.park(VirtualThread.java:534)
      java.base/java.lang.System$2.parkVirtualThread(System.java:2373)
      java.base/jdk.internal.misc.VirtualThreads.park(VirtualThreads.java:60)
      java.base/sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:184)
      java.base/sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:212)
      java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:607)
      java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:331)
      java.base/java.net.Socket.connect(Socket.java:642)
      java.base/sun.security.ssl.SSLSocketImpl.connect(SSLSocketImpl.java:299)
      java.base/sun.security.ssl.BaseSSLSocketImpl.connect(BaseSSLSocketImpl.java:174)
      java.base/sun.net.NetworkClient.doConnect(NetworkClient.java:182)
      java.base/sun.net.www.http.HttpClient.openServer(HttpClient.java:497)
      java.base/sun.net.www.http.HttpClient.openServer(HttpClient.java:600)
      java.base/sun.net.www.protocol.https.HttpsClient.<init>(HttpsClient.java:266)
      java.base/sun.net.www.protocol.https.HttpsClient.New(HttpsClient.java:380)
      java.base/sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.getNewHttpClient(AbstractDelegateHttpsURLConnection.java:189)
      java.base/sun.net.www.protocol.http.HttpURLConnection.plainConnect0(HttpURLConnection.java:1232)
      java.base/sun.net.www.protocol.http.HttpURLConnection.plainConnect(HttpURLConnection.java:1120)
      java.base/sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:175)
      java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1653)
      java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1577)
      java.base/sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:224)
      java.base/java.net.URL.openStream(URL.java:1159)
      com.github.hashjang.LoomThreadMain.getURL(LoomThreadMain.java:48)
      com.github.hashjang.LoomThreadMain.lambda$retrieveURLs$0(LoomThreadMain.java:38)
      java.base/java.util.concurrent.FutureTask.run(FutureTask.java:295)
      java.base/java.util.concurrent.ThreadExecutor$TaskRunner.run(ThreadExecutor.java:385)
      java.base/java.lang.VirtualThread.run(VirtualThread.java:295)
      java.base/java.lang.VirtualThread$VThreadContinuation.lambda$new$0(VirtualThread.java:172)
      java.base/java.lang.Continuation.enter0(Continuation.java:372)
      java.base/java.lang.Continuation.enter(Continuation.java:365)

通過這裡的執行緒堆疊可以看出,虛擬執行緒在執行 I/O 操作的時候,會呼叫 java.lang.Continuation.yield 讓出承載執行緒的資源(相當於 park 住了)。檢視原來的承載執行緒 "ForkJoinPool-1-worker-1" #25,確實處於空閒狀態了。那麼 I/O 操作去哪裡了呢?這就引出了這個執行緒 "Read-Poller" #41

這個執行緒是一個 JVM 共用的 read poller。它的核心邏輯是執行一個基本事件迴圈,監聽所有的同步網路 read(網路讀就緒),connect(發起網路連線就緒) 和 accept(接受網路連線就緒) 操作。當這些 I/O 操作就緒的時候,poller 會被通知,並且 unpark 對應的虛擬執行緒,使得虛擬執行緒繼續執行。同時,除了 read poller,還有一個用於寫事件的 write poller。

我是使用 Windows 進行測試的,在 Windows 中 poller
底層實現基於 wepoll,所以我們看到堆疊裡面包含 WEPoll。對於 MacOS 則是 kqueue,對於 Linux 則是 epoll

poller 維護一個以虛擬執行緒的檔案描述符為 key 的 map。當一個虛擬執行緒並將它的檔案描述符註冊到 poller 上的時候,會以虛擬執行緒的檔案描述符為 key,虛擬執行緒本身為 value 放入這個 map。當 poller 的事件迴圈中的相關事件就緒的時候,通過事件中的虛擬執行緒檔案描述符在 map 中找到對應的虛擬執行緒 unpark 之。

伸縮擴充套件性

如果簡單來看,上面的設計與使用 NIO Channel 和 Selector 並沒有太大的不同,NIO Channel 和 Selector 可以在許多伺服器端框架和庫中找到,例如 Netty。但是相對來說,NIO Channel 和 Selector 提供了一個更復雜的模型,使用者程式碼必須實現事件迴圈並跨 I/O 邊界維護應用程式邏輯,而虛擬執行緒則提供了一個更簡單、更直觀的程式設計模型,Java 平臺負責跨 I/O 邊界排程任務和維護對應的上下文。

如前文我們看到的那樣,虛擬執行緒預設的承載執行緒就是 ForkJoinPool。這是一個非常適合虛擬執行緒的執行緒池,工作竊取演算法能極致的排程執行虛擬執行緒。

結論

同步 Java 網路 API 已經被重新實現,相關的 JEP 包括 JEP 353JEP 373. 在虛擬執行緒中執行時,不能立即完成的 I/O 操作將導致虛擬執行緒被 parked
。當 I/O 就緒時,虛擬執行緒將被 unparked。這個實現相對於當前的非同步非阻塞 I/O 實現程式碼來看,更加簡單易用,隱藏了很多業務不關心的實現細節。

相關文章