深入淺析Java 虛擬線程
在Java 21中,引入了虛擬線程,這是一個非常非常重要的特性,之前一直苦苦尋找的Java協(xié)程,終于問世了。在高并發(fā)以及IO密集型的應用中,虛擬線程能極大的提高應用的性能和吞吐量。
什么是虛擬線程
先來看一下虛擬線程的概念。
虛擬線程概念
DK 21 引入了虛擬線程的支持,這是為了改善 Java 應用程序在高并發(fā)場景下的性能。虛擬線程是一種輕量級線程,具有較小的內存占用,能夠更高效地進行上下文切換,適用于 I/O 密集型的應用程序。
虛擬線程的工作原理
當應用程序啟動一個虛擬線程時,JVM會將這個虛擬線程交給JVM底層的線程池去執(zhí)行,這個底層的線程池是一個傳統(tǒng)線程池,并且真正執(zhí)行虛擬線程中任務的線程,也是傳統(tǒng)線程(操作系統(tǒng)線程)。當虛擬線程遇到阻塞時,JVM會立刻將虛擬線程掛起,讓其它虛擬線程執(zhí)行。也就是說,開啟一個虛擬線程,并不需要啟用一個傳統(tǒng)線程,一般一個傳統(tǒng)線程,可以執(zhí)行多個虛擬線程的任務。在執(zhí)行過程中,可以把虛擬線程理解成任務task。
這里舉一個列子,假設用戶創(chuàng)建了1000個虛擬線程,JVM的執(zhí)行虛擬線程的線程池線程數(shù)是10,那么當?shù)谝粋€虛擬線程V1需要執(zhí)行時,JVM會將V1調度到傳統(tǒng)線程T1上,以此類推,虛擬線程V2會被調度到傳統(tǒng)線程T2上,那么V3->T3,V4->T4,… V10->T10。當執(zhí)行到V11時,這里有三種情況:
如果V1~V10中有任何一個線程遇到阻塞,我們這里假設V3遇到阻塞,那么JVM會將V3掛起,此時T3線程可用,那么V11被T3執(zhí)行。
如果V1~V10沒有線程被阻塞,那么JVM根據劃分的時間片,假設每個虛擬線程允許執(zhí)行100ns,那么過了100ns后,這里V1最新執(zhí)行,JVM則將V1掛起,讓T1去執(zhí)行V11。
如果以上兩種情況都不滿足,那么先將V11掛起,等待有可用的傳統(tǒng)線程時,再執(zhí)行V11。
對于被阻塞的線程,如V3,當IO結束后,操作系統(tǒng)會通過事件,如epoll通知JVM,V3的IO操作已結束,此時JVM重新喚醒V3,選擇可用的傳統(tǒng)線程,來執(zhí)行V3的任務。
這里需要注意兩點:
虛擬線程IO執(zhí)行完成后,會通過操作系統(tǒng)的事件通知機制,如epoll來通知JVM。這一點對于虛擬線程的高效調度至關重要,因為它確保了 阻塞的 I/O 操作 不會占用操作系統(tǒng)線程的時間片,避免了傳統(tǒng)線程池的高資源消耗和效率低下。。
JVM在對虛擬線程進行上下文切換時,因為不涉及到操作系統(tǒng)級別的線程上下文切換,代價非常低,速度也非???。
虛擬線程的調度
一般來說,程序員不需要對虛擬線程的調度進行管理,在JDK 21中,JVM默認啟用了虛擬線程,并且會使用默認的ForkJoinPool線程池來執(zhí)行虛擬線程,并且線程池的大小,也會根據虛擬線程的數(shù)量,進行動態(tài)調整。如果需要手動管理執(zhí)行虛擬線程的線程池大小,那么需要自定義線程池,并將虛擬線程交給自定義的線程池來執(zhí)行,這樣雖然可行,通常沒有必要。
虛擬線程與傳統(tǒng)線程區(qū)別
虛擬線程與傳統(tǒng)線程的區(qū)別主要在于:
創(chuàng)建虛擬線程時,JVM不會創(chuàng)建一個操作系統(tǒng)線程,創(chuàng)建一個傳統(tǒng)線程時,JVM會創(chuàng)建一個操作系統(tǒng)線程。一個傳統(tǒng)線程,可以輪詢執(zhí)行多個虛擬線程。
虛擬線程是由傳統(tǒng)線程來執(zhí)行的,虛擬線程的調度由JVM控制,傳統(tǒng)線程的執(zhí)行和調度,由操作系統(tǒng)來控制。
虛擬線程的上下文切換是由JVM控制的,因為不涉及到操作系統(tǒng)級別線程的上下文切換,虛擬線程上下文切換速度非??欤梢詽M足高并發(fā)需求。
創(chuàng)建一個虛擬線程占用的內存非常小,相對而言,創(chuàng)建一個傳統(tǒng)線程,占用的內存空間大。在應用中,可以創(chuàng)建大量的虛擬線程,一般支持到百萬級,而創(chuàng)建傳統(tǒng)線程,一般只能到幾千,我們一般也不建議創(chuàng)建這么多傳統(tǒng)線程。
虛擬線程類似于task,傳統(tǒng)系統(tǒng)與操作系統(tǒng)線程對應,一個傳統(tǒng)線程可以執(zhí)行多個虛擬線程。虛擬線程與task的區(qū)別是,當傳統(tǒng)線程執(zhí)行虛擬線程時,遇到阻塞會掛起虛擬線程,當傳統(tǒng)線程執(zhí)行task時,遇到阻塞就真的阻塞了。當然傳統(tǒng)中的task繼承自runnable,虛擬線程繼承自Thread,他們屬于不同的類,可調用的方法也不一樣。
JDK也提供了虛擬線程池,可以通過下面方式得到一個虛擬線程池。
import java.util.concurrent.*;
public class VirtualThreadPoolExample {
public static void main(String[] args) {
// 創(chuàng)建一個虛擬線程池
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
// 提交多個任務到線程池
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " running in " + Thread.currentThread());
});
}
// 關閉線程池
executor.shutdown();
}
}上面代碼中,提交給線程池的任務,JVM都會為其創(chuàng)建一個虛擬線程,然后以虛擬線程的方式執(zhí)行。
與傳統(tǒng)的線程池相比,虛擬線程池無法設置核心線程數(shù)、最大線程數(shù)、線程池大小、任務隊列等參數(shù),也不需要設置這些參數(shù)。
虛擬線程與傳統(tǒng)線程的相同之處:
他們都繼承自Thread,用法一摸一樣。也都支持線程池。
與傳統(tǒng)一樣,虛擬線程也有new,runnable,waiting,blocked,terminated等狀態(tài)。
所有的鎖,同步機制,對虛擬線程都適用,并且與傳統(tǒng)線程一樣,虛擬線程也會有資源爭奪以及狀態(tài)同步問題。并且也有上下文切換,雖然虛擬線程的上下文切換,代價非常小。
異常處理機制一樣,如果遇到異常不處理,虛擬線程也會終止執(zhí)行。
虛擬線程與協(xié)程的區(qū)別
協(xié)程是python中的異步編程技術,對于IO密集型應用,協(xié)程可以發(fā)揮很大的優(yōu)勢。協(xié)程的異步工作原理與虛擬線程相似,也是遇到IO就阻塞,讓主線程繼續(xù)執(zhí)行其它任務,當IO完成時,操作系統(tǒng)通過事件機制,如epoll,通知python進程,產生一個事件,放到event loop隊列中,最后由主線程執(zhí)行。
虛擬線程與協(xié)程的主要區(qū)別在于:
| 區(qū)別 | 虛擬線程 | 協(xié)程 |
|---|---|---|
| 并發(fā)/并行 | 虛擬線程是并行的,多個虛擬線程可以同時在多個CPU上運行,同一時刻,可以運行多個虛擬線程。從這個角度將,虛擬線程能支持更高的并發(fā)。 | 協(xié)程不是并行的,因為只有一個主線程執(zhí)行任務事件,同一時刻,只有一個任務被處理。 |
| 資源爭奪 | 虛擬線程中,存在資源爭奪問題,以及狀態(tài)同步問題,在編寫代碼時,需要考慮并發(fā)控制。甚至需要做合理的并發(fā)設計。 | 因為只有一個主線程在執(zhí)行任務事件,沒有并發(fā)問題,編程時也不需要考慮并發(fā)問題。 |
| 框架支持 | 虛擬線程是JDK 21的新特性,不需要任何框架支持。 | 需要框架支持,寫異步代碼和同步代碼,使用的是兩個完全不同的框架,另外學習異步編程,增加了學習成本。并且異步編程有些難度,debug也變得復雜些。 |
怎樣使用虛擬線程
在JDK 21中,使用虛擬線程有兩種方式:
- 直接創(chuàng)建并啟動虛擬線程。
public class VirtualThreadExample {
public static void main(String[] args) {
Thread virtualThread = Thread.ofVirtual().start(() -> {
System.out.println("Hello virtual thread ");
});
try {
virtualThread.join(); // 等待虛擬線程完成
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}- 通過線程池執(zhí)行虛擬線程。
import java.util.concurrent.*;
public class VirtualThreadPoolExample {
public static void main(String[] args) {
// 創(chuàng)建一個虛擬線程池
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
// 提交多個任務到線程池
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " running in " + Thread.currentThread());
});
}
// 關閉線程池
executor.shutdown();
}
}通過線程池執(zhí)行任務時,無法對并發(fā)實現(xiàn)控制,容易造成OOM,或耗盡服務方資源,可以自定義以下虛擬線程池,實現(xiàn)資源控制:
package com.zengbiaobiao.demo.vitrualthreaddemo;
import org.springframework.lang.NonNull;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
/*****
* 虛擬線程池,支持配置任務隊列數(shù)和最大并發(fā)任務數(shù)
*/
public class VirtualThreadExecutorService extends AbstractExecutorService {
private volatile boolean shouldStop = false;
private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
private final Semaphore semaphore;
private final BlockingQueue<Runnable> taskQueue;
/******
* 構造函數(shù)
* @param taskQueueSize,任務隊列大小,任務隊列是一個阻塞隊列,如果任務隊列滿了,那么調用execute方法會阻塞
* @param concurrencySize,并發(fā)任務大小,同時執(zhí)行的IO任務個數(shù),防止并發(fā)過重,或者資源不夠
*/
public VirtualThreadExecutorService(int taskQueueSize, int concurrencySize) {
this.semaphore = new Semaphore(concurrencySize);
taskQueue = new LinkedBlockingQueue<>(taskQueueSize);
this.loopEvent();
}
private void loopEvent() {
Thread.ofVirtual().name("VirtualThreadExecutor").start(() -> {
while (!shouldStop) {
try {
Runnable task = taskQueue.take();
semaphore.acquire();
executor.execute(() -> {
try {
try {
task.run();
} finally {
semaphore.release();
}
} catch (Exception e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
if (shouldStop) break;
}
}
});
}
@Override
public void shutdown() {
shouldStop = true;
executor.shutdown();
}
/**
* @return The task not executed
*/
@Override
public List<Runnable> shutdownNow() {
shouldStop = true;
List<Runnable> remainingTasks = new ArrayList<>(taskQueue);
taskQueue.clear();
executor.shutdownNow();
return remainingTasks;
}
@Override
public boolean isShutdown() {
return shouldStop;
}
@Override
public boolean isTerminated() {
return shouldStop && executor.isTerminated();
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
return executor.awaitTermination(timeout, unit);
}
@Override
public void execute(Runnable command) {
try {
taskQueue.put(command); // 阻塞直到隊列有空間
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RejectedExecutionException("Task submission interrupted.", e);
}
}
}測試代碼如下:
package com.zengbiaobiao.demo.vitrualthreaddemo;
import org.apache.tomcat.util.threads.VirtualThreadExecutor;
public class VirtualThreadExecutorServiceDemo {
public static void main(String[] args) throws InterruptedException {
VirtualThreadExecutorService executorService = new VirtualThreadExecutorService(10, 2);
for (int i = 0; i < 100000; i++) {
final String threadName = "thread-" + i;
System.out.println(Thread.currentThread() + ": try to create task " + threadName);
executorService.submit(() -> {
System.out.println(Thread.currentThread() + ": " + threadName + " created!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread() + ": " + threadName + " finished!");
});
}
Thread.sleep(5000000);
}
}哪些場景下可以應用虛擬線程
虛擬線程在IO密集型的高并發(fā)應用中能發(fā)揮出巨大的威力,在所有IO密集型應用中,具體來說,下列場景中,使用虛擬線程是比較合適的:
短時間需要完成的任務,且沒有資源爭奪或亂序問題,比如數(shù)據庫寫入,服務器 HTTP 請求處理,遠程 RESTful API 調用,RabbitMQ 消息處理等應用場景。。
長時間運行的任務,但是對消息處理由順序要求的任務。比如在電梯監(jiān)控系統(tǒng)中,需要對每臺電梯的數(shù)據進行處理,但是需要保證消息被處理的順序。這時可以為每臺電梯創(chuàng)建一個虛擬線程,這臺電梯的數(shù)據交給專門的虛擬線程處理。因為應用中可以創(chuàng)建大量虛擬線程,并且虛擬線程一般都是異步處理任務,所以這個場景中,使用虛擬線程,可以滿足高性能和高并發(fā)的要求。
API網關中,對多個上游API數(shù)據進行查詢,組裝合并,使用虛擬線程,相比傳統(tǒng)線程,效果更佳。虛擬線程,也支持CountDownLatch,Semaphore等工具類。
事件驅動的架構中,使用虛擬線程,效果也很好。比如spring boot中的異步事件,默認使用的是傳統(tǒng)線程池,如果將其改成虛擬線程池,并發(fā)處理能力可以極大提高。
那么哪些場景下不合適使用虛擬線程呢?
CPU密集型應用,比如大數(shù)據處理、圖像處理、矩陣運算等。
如果應用有很高的并發(fā)資源爭奪,或者狀態(tài)同步,并且造成系統(tǒng)吞吐量低,需要考慮優(yōu)化并發(fā)模型,這種場景下,不但傳統(tǒng)線程不合適,虛擬線程也不合適。
虛擬線程實際應用場景舉例
在一個spring boot項目中,有時候因為異步事件處理不過來,造成吞吐量下降,在JDK 21中,可以將事件改成虛擬線程來執(zhí)行,代碼如下:
package com.zengbiaobiao.demo.vitrualthreaddemo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
// 最大并行任務數(shù)
Semaphore semaphore = new Semaphore(100);
ExecutorService virtualThreadPool = Executors.newVirtualThreadPerTaskExecutor();
return runnable -> {
try {
// 控制并行任務數(shù)
semaphore.acquire();
virtualThreadPool.submit(() -> {
try {
runnable.run();
} finally {
semaphore.release();
}
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Task submission interrupted", e);
}
};
}
}事件發(fā)送和處理代碼如下:
package com.zengbiaobiao.demo.vitrualthreaddemo;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/home")
public class HomeController {
private final ApplicationEventPublisher eventPublisher;
public HomeController(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
@GetMapping("/index")
public String index() {
for (int i = 0; i < 1000; i++) {
eventPublisher.publishEvent("event " + i);
}
return "success";
}
@EventListener
@Async
public void handleEvent(String event) {
System.out.println(Thread.currentThread() + ": " + event);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}輸出結果如下:
VirtualThread[#2031]/runnable@ForkJoinPool-1-worker-4: event 976
VirtualThread[#2039]/runnable@ForkJoinPool-1-worker-1: event 980
VirtualThread[#1064]/runnable@ForkJoinPool-1-worker-1: event 983
VirtualThread[#2047]/runnable@ForkJoinPool-1-worker-2: event 984
VirtualThread[#2049]/runnable@ForkJoinPool-1-worker-9: event 985
VirtualThread[#2057]/runnable@ForkJoinPool-1-worker-2: event 989
VirtualThread[#2059]/runnable@ForkJoinPool-1-worker-3: event 990
VirtualThread[#2061]/runnable@ForkJoinPool-1-worker-6: event 991
VirtualThread[#2063]/runnable@ForkJoinPool-1-worker-10: event 992
VirtualThread[#2065]/runnable@ForkJoinPool-1-worker-10: event 993
VirtualThread[#2071]/runnable@ForkJoinPool-1-worker-3: event 996
VirtualThread[#2069]/runnable@ForkJoinPool-1-worker-2: event 995
VirtualThread[#2075]/runnable@ForkJoinPool-1-worker-7: event 998
VirtualThread[#2077]/runnable@ForkJoinPool-1-worker-10: event 999
上面輸出結果中,每次并發(fā)執(zhí)行100個任務,當虛擬線程池任務達到100之后,執(zhí)行eventPublisher.publishEvent("event " + i)代碼時,代碼阻塞,過100ms之后,100個任務執(zhí)行完成,下一批任務被執(zhí)行。
虛擬線程使用注意事項
搞清楚任務類型,是IO密集型,還是CPU密集型
與傳統(tǒng)線程結合使用
關注性能和資源,使用虛擬線程無法通過線程池等工具控制并發(fā),需要借助Semepha,CountdownLatch等工具才能限流,如果不限流,容易造成OOM,或對目標系統(tǒng)造成巨大流量沖擊。
在異步框架中,關注隱藏的傳統(tǒng)線程,比如在HttpClient的異步請求中,每次異步請求都會創(chuàng)建一個HttpClient回調線程。大量的傳統(tǒng)線程被間接創(chuàng)建,也容易引起OOM。
由synchronized關鍵字引起的pinned問題,看起來在JDK 21中,做了一些優(yōu)化,即便虛擬線程pinned到傳統(tǒng)線程,也只是性能退回到傳統(tǒng)線程,無非是慢一點,反而不是太大問題。經過大量測試,發(fā)現(xiàn)基本只出現(xiàn)一次,之后不會再出現(xiàn)。不過使用ReentrantLock,效果確實會好很多,將synchronized關鍵字改成lock.()和lock.unlock(),F(xiàn)orkJoinPool中的線程數(shù)量會降低,并且任務分配均衡。
不要忽略軟件設計,尤其在需要大量同步的應用中。
經過驗證,虛擬線程在遇到IO時,確實會讓步,并且不消耗太多資源,核心特點是,讓異步編程變得簡單,并且不需要框架支持。但是容易因大的并發(fā),造成OOM,或者對目標系統(tǒng)造成沖擊,追求高并發(fā)可用,但一定要做測試和驗證。對于需要做狀態(tài)同步,如需要加鎖,或需要使用synchronize關鍵字的代碼,需要優(yōu)化設計,如果無法規(guī)避,那么,使用虛擬線程,和使用線程池,效果差不多。
虛擬線程存在的問題:
Java Virtual Threads — some early gotchas to look out for
Two Pitfalls by moving to Java Virtual Threads
Java 21 Virtual Threads - Dude, Where’s My Lock?
Pitfalls to avoid when switching to Virtual threads
Do Java 21 virtual threads address the main reason to switch to reactive single-thread frameworks?
Pinning: A pitfall to avoid when using virtual threads in Java
Taming the Virtual Threads: Embracing Concurrency With Pitfall Avoidance
Pitfalls you encounter with virtual threads
示例代碼在Gitee上同步
到此這篇關于Java 虛擬線程 探索的文章就介紹到這了,更多相關Java 虛擬線程內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
springboot2.x只需兩步快速整合log4j2的方法
這篇文章主要介紹了springboot2.x只需兩步快速整合log4j2的方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-05-05
PowerJob的IdGenerateService工作流程源碼解讀
這篇文章主要為大家介紹了PowerJob的IdGenerateService工作流程源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2024-01-01
使用SpringBoot+nmap4j獲取端口信息的代碼詳解
這篇文章主要介紹了使用 SpringBoot + nmap4j 獲取端口信息,包括需求背景、nmap4j 的相關介紹、代碼說明(含測試代碼、改造后的代碼及參數(shù)說明),還提到了文件讀取方式和依賴引入方式,最終請求能獲取到數(shù)據,需要的朋友可以參考下2025-01-01
mybatis(mybatis-plus)映射文件(XML文件)中特殊字符轉義的實現(xiàn)
XML 文件在解析時會將五種特殊字符進行轉義,本文主要介紹了mybatis(mybatis-plus)映射文件(XML文件)中特殊字符轉義的實現(xiàn),具有一定的參考價值,感興趣的可以了解一下2023-12-12

