網(wǎng)站開發(fā)崗位日常工作網(wǎng)頁設(shè)計(jì)與制作步驟
??博客首頁???????https://blog.csdn.net/Java_Yangxiaoyuan
???????歡迎優(yōu)秀的你👍點(diǎn)贊、🗂?收藏、加??關(guān)注哦。
???????本文章CSDN首發(fā),歡迎轉(zhuǎn)載,要注明出處哦!
???????先感謝優(yōu)秀的你能認(rèn)真的看完本文,有問題歡迎評論區(qū)交流,都會認(rèn)真回復(fù)!
上一篇博文:【昕寶爸爸小模塊】深入淺出之JDK21 中的虛擬線程到底是怎么回事(一)
深入淺出之JDK21 中的虛擬線程到底是怎么回事(二)
- 一、?線程的實(shí)現(xiàn)方式
- 1.1?使用內(nèi)核線程實(shí)現(xiàn)
- 1.2?使用用戶線程實(shí)現(xiàn)
- 1.3?使用用戶線程加輕量級進(jìn)程混合實(shí)現(xiàn)
- 一、?拓展知識倉
- 2.1?內(nèi)核線程有什么優(yōu)點(diǎn)和缺點(diǎn)
- 2.2?內(nèi)核線程和用戶線程的區(qū)別
- 2.3?內(nèi)核線程有哪些應(yīng)用場景
- 2.4?Java的線程實(shí)現(xiàn)
- 2.5?虛擬線程
- 2.6 ?虛擬線程和平臺線程的區(qū)別
- 2.7?如何使用
- 2.8 ?性能差異
一、?線程的實(shí)現(xiàn)方式
我們都知道,在操作系統(tǒng)中,線程是比進(jìn)程更輕量級的調(diào)度執(zhí)行單位,線程的引入可以把一個(gè)進(jìn)程的盜源分配和執(zhí)行調(diào)度分開,各個(gè)線程既可以共享進(jìn)程資源,又可以獨(dú)立調(diào)度。
其實(shí),線程的實(shí)現(xiàn)方式主要有三種: 分別是使用內(nèi)核線程實(shí)現(xiàn)、使用用戶線程實(shí)現(xiàn)以及使用用戶線程加輕量級進(jìn)程混合實(shí)現(xiàn)。
1.1?使用內(nèi)核線程實(shí)現(xiàn)
內(nèi)核線程(Kernel-Level Thread,KLT) 就是直接由操作系統(tǒng)內(nèi)核 (Kernel) 支持的線程,這種線程由內(nèi)核來完成線程切換,內(nèi)核通過操縱調(diào)度器(Scheduler) 對線程進(jìn)行調(diào)度,并負(fù)責(zé)將線程的任務(wù)映射到各個(gè)處理器上,并向應(yīng)用程序提供API接口來管理線程。
應(yīng)用程序一般不會直接去使用內(nèi)核線程,而是去使用內(nèi)核線程的一種高級接口—— 輕量級進(jìn)程 (LightWeight Process,LWP)
,輕量級進(jìn)程就是我們通常意義上所進(jìn)的線程,由于每個(gè)輕量級進(jìn)程都由一個(gè)內(nèi)核線程支持,因此只有先支持內(nèi)核線程,才能有輕量級進(jìn)程。
有了內(nèi)核線程的支持,每個(gè)輕量級進(jìn)程都成為一個(gè)獨(dú)立的調(diào)度單元,即使有一個(gè)輕量級進(jìn)程在系統(tǒng)調(diào)用中阻塞了,也不會影響整個(gè)進(jìn)程繼續(xù)工作。
但是輕量級進(jìn)程具有它的局限性:首先,由于是基于內(nèi)核線程實(shí)現(xiàn)的,所以各種線程操作,如創(chuàng)建、析構(gòu)及同步,都需要進(jìn)行系統(tǒng)調(diào)用。而系統(tǒng)調(diào)用的代價(jià)相對較高,需要在用戶態(tài) (User Mode)和內(nèi)核態(tài)(Kernel Mode)中來回切換。 其次,每個(gè)輕量級進(jìn)程都需要有一個(gè)內(nèi)核線程的支持,因此輕量級進(jìn)程要消耗一定的內(nèi)核資源(如內(nèi)核線程的??臻g),因此一個(gè)系統(tǒng)支持輕量級進(jìn)程的數(shù)量是有限的。
看一個(gè)簡單的栗子:
- Java代碼:
/**
* 如何使用內(nèi)核線程來創(chuàng)建一個(gè)簡單的多線程程序
*/public class KernelThreadExample {public static void main(String[] args) {KernelThread kernelThread = new KernelThread();kernelThread.start();}
}
- JNI頭文件 (
KernelThread.h
):
#include <jni.h>
#include <pthread.h> // 用于創(chuàng)建線程
#include <stdio.h> // 用于輸出信息// 聲明本地方法
JNIEXPORT void JNICALL Java_KernelThreadExample_startNativeThread(JNIEnv *env, jobject obj);
- C/C++實(shí)現(xiàn) (
KernelThread.c
):
#include "KernelThread.h"
#include <pthread.h> // 用于創(chuàng)建線程
#include <stdio.h> // 用于輸出信息// 本地線程函數(shù)
void* threadFunction(void* arg) {printf("This is running in a native thread!\n");return NULL;
}JNIEXPORT void JNICALL Java_KernelThreadExample_startNativeThread(JNIEnv *env, jobject obj) {pthread_t threadId;pthread_create(&threadId, NULL, threadFunction, NULL); // 創(chuàng)建線程pthread_detach(threadId); // 分離線程,這樣主程序結(jié)束時(shí)線程也會結(jié)束
}
- 編譯和鏈接:使用gcc或g++編譯C/C++文件,并鏈接到Java的本地庫。確保指定JNI頭文件。例如:
gcc -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libkernelThreadImpl.so KernelThread.c
- 運(yùn)行Java程序:運(yùn)行你的Java程序,確保已將生成的本地庫(例如,
libkernelThreadImpl.so
)放在系統(tǒng)的庫路徑中或指定到java.library.path
系統(tǒng)屬性。例如:
java -Djava.library.path=. KernelThreadExample
- 觀察輸出:會看到“This is running in a native thread!”的消息輸出,這表明你的代碼正在新的內(nèi)核線程中執(zhí)行。
1.2?使用用戶線程實(shí)現(xiàn)
在用戶空間建立線程庫,通過運(yùn)行時(shí)系統(tǒng)(Run-time System)來完成線程的管理,因?yàn)檫@種線程的實(shí)現(xiàn)是在用戶空間的,所以操作系統(tǒng)的內(nèi)核并不知道線程的存在,所以內(nèi)核管理的還是進(jìn)程,所以這種線程的切換不需要內(nèi)核操作。
這種實(shí)現(xiàn)方式下,進(jìn)程和線程之間的關(guān)系是一對多的。
這種線程實(shí)現(xiàn)方式的優(yōu)點(diǎn)是線程切換快,并且可以運(yùn)行在任何操作系統(tǒng)之上,只需要實(shí)現(xiàn)線程庫就行了。但是缺點(diǎn)也比較明顯,就是所有線程的操作都需要用戶程序自己處理,并且因?yàn)榇蠖鄶?shù)系統(tǒng)調(diào)用都是阻塞的,所以一旦一個(gè)進(jìn)程阻塞了,那么進(jìn)程中的所有線程也會被阻塞。還有就是多處理器系統(tǒng)中如何將線程映射到其他處理器上也是一個(gè)比較大的問題。
1.3?使用用戶線程加輕量級進(jìn)程混合實(shí)現(xiàn)
還有一種混合實(shí)現(xiàn)的方式,就是線程的創(chuàng)建在用戶空間完成,通過線程庫進(jìn)行,但是線程的調(diào)度是由內(nèi)核來完成的。多個(gè)用戶線程通過多路復(fù)用來復(fù)用多個(gè)內(nèi)核線程。這個(gè)就不展開講了。
一、?拓展知識倉
2.1?內(nèi)核線程有什么優(yōu)點(diǎn)和缺點(diǎn)
內(nèi)核線程的優(yōu)點(diǎn)主要包括:
- 高并發(fā)性:在多處理器系統(tǒng)上,內(nèi)核能夠同時(shí)調(diào)度同一進(jìn)程中的多個(gè)線程執(zhí)行,從而實(shí)現(xiàn)高并發(fā)性。
- 資源利用率高:當(dāng)一個(gè)線程被阻塞時(shí),內(nèi)核可以調(diào)度同一進(jìn)程中的其他線程繼續(xù)執(zhí)行,從而充分利用系統(tǒng)資源。
- 避免用戶態(tài)切換開銷:內(nèi)核線程在內(nèi)核態(tài)運(yùn)行,避免了用戶態(tài)和內(nèi)核態(tài)之間的切換開銷。
內(nèi)核線程缺點(diǎn):
- 系統(tǒng)開銷大:相對于用戶線程,內(nèi)核線程需要更多的系統(tǒng)資源,如內(nèi)核棧和上下文切換時(shí)的資源。
- 不適合小規(guī)模并發(fā):對于小規(guī)模的并發(fā),內(nèi)核線程可能不是最優(yōu)的選擇,因?yàn)槠鋭?chuàng)建和銷毀成本較高。
- 不適合低延遲場景:內(nèi)核線程的上下文切換時(shí)間較長,因此不適合需要低延遲的場景。
- 受限于操作系統(tǒng)調(diào)度策略:內(nèi)核線程的行為受限于操作系統(tǒng)的調(diào)度策略,這可能影響其性能和行為。
內(nèi)核線程適用于需要高并發(fā)和資源利用率的應(yīng)用,如服務(wù)器和大數(shù)據(jù)處理等場景。但在小規(guī)模并發(fā)、低延遲或特定性能要求的場景中,用戶線程或其他并發(fā)模型可能更合適。
2.2?內(nèi)核線程和用戶線程的區(qū)別
內(nèi)核線程和用戶線程的區(qū)別主要體現(xiàn)在以下幾個(gè)方面:
- 創(chuàng)建和銷毀:內(nèi)核線程由操作系統(tǒng)內(nèi)核創(chuàng)建和銷毀,而用戶線程由應(yīng)用程序創(chuàng)建和銷毀。
- 運(yùn)行模式:內(nèi)核線程運(yùn)行在內(nèi)核態(tài),可以訪問操作系統(tǒng)的所有資源,而用戶線程運(yùn)行在用戶態(tài),只能訪問應(yīng)用程序的資源。
- 任務(wù)類型:內(nèi)核線程可以執(zhí)行任何操作系統(tǒng)提供的服務(wù),如文件系統(tǒng)、網(wǎng)絡(luò)等,而用戶線程只能執(zhí)行應(yīng)用程序提供的服務(wù)。
- 調(diào)度和切換:內(nèi)核線程的創(chuàng)建和銷毀需要操作系統(tǒng)內(nèi)核的支持,而用戶線程的創(chuàng)建和銷毀由應(yīng)用程序自己控制。內(nèi)核線程的切換需要操作系統(tǒng)內(nèi)核的支持,而用戶線程的切換由應(yīng)用程序自己控制。內(nèi)核線程的調(diào)度由操作系統(tǒng)內(nèi)核負(fù)責(zé),而用戶線程的調(diào)度由應(yīng)用程序自己控制。
- 權(quán)限:內(nèi)核線程始終具有最高權(quán)限,且權(quán)限不低于用戶線程。用戶線程的權(quán)限可以調(diào)整,但無法超過內(nèi)核線程的權(quán)限。
- 效率:對于用戶線程來說,如果系統(tǒng)調(diào)用是阻塞的,那么整個(gè)進(jìn)程都會阻塞。這時(shí)需要內(nèi)核線程來處理,因?yàn)閮?nèi)核線程是在操作系統(tǒng)中實(shí)現(xiàn)的機(jī)制,它在操作系統(tǒng)中有線程控制塊(TCB),在發(fā)生錯(cuò)誤或運(yùn)行權(quán)限到期需要進(jìn)行上下文切換時(shí),會主動向內(nèi)核發(fā)送中斷信號并停止執(zhí)行,將運(yùn)行權(quán)限交給內(nèi)核,由內(nèi)核的interrupt handler進(jìn)行相應(yīng)的處理。
內(nèi)核線程和用戶線程在多個(gè)方面存在顯著差異,具體選擇使用哪種方式要根據(jù)具體的應(yīng)用場景和需求來決定。
2.3?內(nèi)核線程有哪些應(yīng)用場景
內(nèi)核線程的應(yīng)用場景主要包括:
- 服務(wù)器端編程:在服務(wù)器端編程中,內(nèi)核線程常被用于處理大量并發(fā)的請求。由于內(nèi)核線程可以同時(shí)處理多個(gè)請求,因此可以大大提高服務(wù)器的處理能力和吞吐量。
- 實(shí)時(shí)系統(tǒng):在實(shí)時(shí)系統(tǒng)中,內(nèi)核線程用于確保關(guān)鍵任務(wù)能夠及時(shí)執(zhí)行。通過使用內(nèi)核線程,實(shí)時(shí)系統(tǒng)可以實(shí)現(xiàn)確定性的行為和快速的系統(tǒng)響應(yīng)。
- 設(shè)備驅(qū)動程序:許多設(shè)備驅(qū)動程序使用內(nèi)核線程來處理硬件事件或執(zhí)行后臺任務(wù)。這樣可以將設(shè)備的操作與系統(tǒng)的其他部分隔離,并確保設(shè)備的正確和高效運(yùn)行。
-
文件系統(tǒng)和網(wǎng)絡(luò)協(xié)議:文件系統(tǒng)和網(wǎng)絡(luò)協(xié)議通常使用內(nèi)核線程來處理文件和網(wǎng)絡(luò)操作。這樣可以提高系統(tǒng)的整體性能,并確保這些操作的可靠性和高效性。
-
系統(tǒng)守護(hù)進(jìn)程和服務(wù):許多系統(tǒng)守護(hù)進(jìn)程和服務(wù)使用內(nèi)核線程來執(zhí)行長期運(yùn)行的后臺任務(wù)。例如,syslog服務(wù)使用內(nèi)核線程來讀取并處理系統(tǒng)日志消息。
- 并行計(jì)算:在需要大規(guī)模并行處理的場景下,如高性能計(jì)算(HPC),內(nèi)核線程可以用于實(shí)現(xiàn)高效的并行計(jì)算。通過將計(jì)算任務(wù)分解為多個(gè)子任務(wù),并由多個(gè)內(nèi)核線程同時(shí)執(zhí)行,可以提高整體計(jì)算性能。
總之吧,內(nèi)核線程在許多場景中都有廣泛的應(yīng)用,特別是在需要高并發(fā)、實(shí)時(shí)性、可靠性和高效的系統(tǒng)場景中。
2.4?Java的線程實(shí)現(xiàn)
開頭講的是操作系統(tǒng)的線程的實(shí)現(xiàn)的三種方式,不同的操作系統(tǒng)在實(shí)現(xiàn)線程的時(shí)候會采用不同的機(jī)制比如windows采用的是內(nèi)核線程實(shí)現(xiàn)的,而Solaris則是通過混合模式實(shí)現(xiàn)的。
而Java作為一門跨平臺的編程語言,實(shí)際上他的線程的實(shí)現(xiàn)其實(shí)是依賴具體的操作系統(tǒng)的。而比較常用的windows和linux來說,都是采用的內(nèi)核線程的方式實(shí)現(xiàn)的。
也就是說,當(dāng)我們在JAVA代碼中創(chuàng)建一個(gè)Thread的時(shí)候,其實(shí)是需要映射到提作系統(tǒng)的線程的具體實(shí)現(xiàn)的,因?yàn)槌R姷耐ㄟ^內(nèi)核線程實(shí)現(xiàn)的方式在創(chuàng)建、調(diào)度時(shí)都需要進(jìn)行內(nèi)核參與,所以成本比較高,盡管JAVA中提供了線程池的方式來避免重復(fù)創(chuàng)建線程,但是依舊有很大的優(yōu)化空間。而且這種實(shí)現(xiàn)方式意味著受機(jī)器資源的影響,平臺線程數(shù)也是有限制的。
2.5?虛擬線程
JDK 19引入的虛擬線程,是JD 實(shí)現(xiàn)的輕量級線程,他可以避免上下文切換帶來的的額外耗費(fèi)。他的實(shí)現(xiàn)原理其實(shí)是JDK不再是每一個(gè)線程都一對一的對應(yīng)一個(gè)操作系統(tǒng)的線程了,而是會將多個(gè)虛擬線程映射到少量操作系統(tǒng)線程中,通過有效的調(diào)度來避免那些上下文切換。
而且,我們可以在應(yīng)用程序中創(chuàng)建非常多的虛擬線程,而不依賴于平臺線程的數(shù)量。這些虛擬線程是由JVM管理的,因此它們不會增加額外的上下文切換開銷,因?yàn)樗鼈冏鳛槠胀↗ava對象存儲在RAM中。
2.6 ?虛擬線程和平臺線程的區(qū)別
首先,虛擬線程總是守護(hù)線程。setDaemon (false)
方法不能將虛擬線程更改為非守護(hù)線程。所以,需要注意的是,當(dāng)所有啟動的非守護(hù)線程都終止時(shí),JVM將終止。這意味著JM不會等待虛擬線程完成后才退出。
其次,即使使用setPriority()方法,虛擬線程始終具有 normal
的優(yōu)先級,且不能更改優(yōu)先級。在虛擬線程上調(diào)用此方法沒有效果。
還有就是,虛擬線程是不支持stop()、suspend()或resume()等方法。這些方法在虛擬線程上調(diào)用時(shí)會拋出UnsupportedOperationException
異常。
2.7?如何使用
接下來個(gè)紹一下,在JDK 19中如何使用虛擬線程。
首先,通過Thread.startVirtualThread0可以運(yùn)行一個(gè)虛擬線程:
Thread.startVirtualThread(() -> {System.out.println("虛擬線程執(zhí)行中...");
});
其次,通過 Thread.Builder
也可以創(chuàng)建虛擬線程,Thread類提供了ofPlatform()
來創(chuàng)建一人平臺線程ofVirtual0來創(chuàng)建虛擬線程。
Thread.Builder platformBuilder = Thread.ofplatform().name("平臺線程");
Thread.Builder virtualBuilder = Thread.ofVirtual().name("虛擬線程");Thread t1 = platformBuilder .start(() -> {...});
Thread t2 = virtualBuilder.start(() -> {...});
另外,線程池也支持了虛擬線程,可以通過 Executors.newVirtua ThreadPerlaskExecutor()
來創(chuàng)建康擬線程:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {Intstream.range(0, 10000).forEach(i -> {executor.submit(() -> {Thread.sleep(Duration.ofseconds(1));return i:}};});
}
但是,其實(shí)并不建議虛擬線程和線程池一起使用,因?yàn)镴ava線程池的設(shè)計(jì)是為了避免創(chuàng)建新的操作系統(tǒng)線程的開銷,但是創(chuàng)建虛擬線程的開銷并不大,所以其實(shí)沒必要放到線程池中。
2.8 ?性能差異
我在這里說了大半天,虛擬線程到底能不能提升性能,能提升多少呢? 我們來做個(gè)測試。
我們寫一個(gè)簡單的任務(wù),在控制臺中打印消息之前等待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());
});
現(xiàn)在,我們將從這個(gè)Runnable創(chuàng)建10,000個(gè)線程,并使用虛擬線程和平臺線程執(zhí)行它們,以比較兩者的性能。
先來我們比較熟悉的平臺線程的實(shí)現(xiàn):
Instant start = Instant.now();try (var executor = Executors .newFixedThreadPool(100)) {for(int i = ; i < 10_000; i++) {executor.submit(runnable);}
}
Instant finish = Instant .now();
long timeElapsed = Duration.between(start, finish).toMillis();
System.out.printIn("總耗時(shí) :" + timeElapsed);
輸出結(jié)果為:
總耗時(shí) : 102323
總耗時(shí)大概100秒左右。接下來再用虛擬線程跑一下看看。
在JDK 21中已經(jīng)是正式功能了,但是在JDK 19中,虛擬線程是一個(gè)預(yù)覽API,默認(rèn)是禁用。所以需要使用$java–source 19–enable-preview xxjava 的方式來運(yùn)行代碼。
Instant start = Instant.now();try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {for(int i = ; i < 10_000; i++) {executor.submit(runnable);}
}Instant finish = Instant.now();long timeElapsed = Duration.between(start, finish).toMillis():System.out.printIn("總耗時(shí) :" + timeElapsed);
使用 Executors.newVirtualThreadPerTaskExecutor
來創(chuàng)建虛擬線程,執(zhí)行結(jié)果如下:
總耗時(shí):1674
總耗時(shí)大概1.6秒左右。
100秒和1.6秒的差距,足以看出虛擬線程的性能提升還是立竿見影的。