技術面:Java并發(上下文切換、線程安全、并發與并行、守護線程、虛擬線程)
多線程中的上下文切換是什么?
上下文切換
是指CPU從一個線程切換到另一個線程時,需要保存當前線程的上下文狀態,然后恢復另一個線程的上下文狀態,這樣下次恢復執行該線程時也能夠正確的執行。
多線程中的上下文切換
在多線程的情況下,線程上下文的切換是一種常見的操作,通常是在一個CPU上,由于多個線程共享CPU的時間片,一個線程的時間片用完后,切換到另一個線程運行時,要保存當前線程的狀態信息,包括程序計數器,寄存器,棧指針等。以便下次執行該線程時,能恢復到正確的狀態。
由于是在多線程的情況下,所以上下文切換的開銷比單線程的開銷大,因為多線程下需要保存和恢復更多的上下文信息。過多的上下文切換會降低系統的運行效率,因此需盡可能少的避免線程上下文切換次數。
避免頻繁切換上下文的方法:
- 降低線程數,通過合理的線程池來管理線程,減少線程的創建和銷毀,并不是線程數越多越好,合理的線程數可以避免線程過多的上下文切換。
- 采用無鎖并發編程,可以避免線程因等待鎖而進入阻塞狀態,從而減少上下文切換的發生。
- 用CAS算法,CAS這種樂觀鎖的算法,可以避免線程的阻塞和喚醒操作,從而減少上下文切換。
- 合理是使用鎖,在使用鎖的過程中,避免過多地使用同步塊或同步方法,一定要用的化,要盡量縮小同步塊兒和同步方法的范圍,從而減少線程的阻塞時間,減少上下文的切換。
- 使用協程(JDK19的虛擬線程),這是一種用戶態的線程,其切換不需要操作系統的參與,因此可以避免上下文的切換(JVM層面還是會有一些保存和恢復線程的狀態)。
你覺得什么是線程安全?
線程安全是指在多線程并發的情況下,能夠正確的處理多線程之間的共享變量,使程序能夠正確的執行。
這里所說的程序能夠正確執行,主要是滿足所謂的原子性、有序性和可見性。
共享變量
共享變量,即所有線程都可以操作的變量。
在操作系統中,進程是分配資源的基本單位,線程是執行的基本單位,因此多個線程是可以共享進程中的數據。在JVM中,堆和方法區的區域是多個線程共享的數據區域。
那么哪些變量是保存在堆和方法區中的呢,哪些變量又是保存在棧中的呢?
| 堆 | 方法區(元空間) | 棧 |
|---|---|---|
| 類變量 | 實例變量 | 局部變量 |
public class VarTest {
/**
* 類變量
*/
public static String ClassVar = "ClassVar";
/**
* 實例變量
*/
public String entityVar = "entityVar";
/**
* 局部變量
*/
public void logMethodVar(){
// 局部變量
int methodIntVar = 1;
System.out.println(methodIntVar);
}
}
并行和并發有什么區別
并發(concurrency),在操作系統中,同一時間有多個程序處于運行中,且這幾個程序是在同一個CPU中執行。
對于單個CPU來說,同一時間只能干一件事情,為了看起來像是同事干多件事情,操作系統把CPU的時間劃分成長短基本相同的時間區間,也就是“時間片”,通過操作系統的管理,把這些時間片依次輪流地分配給各個用戶使用。
并行(parallel),當操作系統有一個以上的CPU時,當一個CPU執行一個進程時,另一個CPU可以執行另一個進程,兩個進程互相不搶占CPU資源,可以同時進行。


兩者的關鍵性區別
- 并行是物理上的同時執行,而并發是邏輯上的同時執行。
- 并行通常需要更多硬件資源(如多核CPU),并發則更注重任務調度的效率。
- 并行的目標是加速單個任務(通過拆分任務并行處理),而并發的目標是處理更多任務(通過快速切換)。
守護線程與普通線程有什么區別
守護線程(Daemon Thread)和普通用戶線程(User Thread),是兩種不同類型的線程,兩者都是可以通過Thread類或Runnable接口創建的。
兩者最大的區別在于:JVM會等待所有普通用戶線程執行完畢后才退出。JVM不會等待守護線程完成,當所有普通線程結束時,JVM會強制終止所有守護線程。
守護線程一般被用來執行后臺任務,最典型的場景就是JVM的GC(垃圾回收器)。
還有一些場景例如:
- 日志記錄(異步寫入日志)
- 定時任務監控(如心跳檢測)
- 資源清理(緩存清理)
- JDK19出現的虛擬線程,也是守護線程。
普通線程
public static void userThreadRun(){
Thread userThread = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("用戶線程執行: " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
userThread.start();
}
運行結果
用戶線程執行: 0
用戶線程執行: 1
用戶線程執行: 2
守護線程執行
public static void daemonThreadRun() throws InterruptedException {
Thread daemonThread = new Thread(() -> {
while (true) {
System.out.println("守護線程執行");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
daemonThread.setDaemon(true);
daemonThread.start();
// 主線程(用戶線程)執行完后,JVM退出,守護線程被強制終止
Thread.sleep(3000);
System.out.println("主線程結束");
}
運行結果
守護線程執行
守護線程執行
守護線程執行
守護線程執行
守護線程執行
守護線程執行
主線程結束
守護線程daemonThread會無限循環打印,但當主線程結束時,守護線程會被JVM強制終止。
JDK21中的虛擬線程是什么?
虛擬線程?協程?
虛擬線程可能比較陌生,但是如果說叫協程是不是聽著就比較熟悉了,如果有了解Go、Ruby、Python語言的,對協程肯定不陌生了。
Java中在JDK21中,正式將協程以虛擬線程的形式發布出來了。在之前的JDK版本中Java的線程模型比較簡單,每一個Java線程對應一個操作系統中的輕量級進程,這種線程模型中的線程創建、析構及同步等動作,都需要進行系統調用。而系統調用則需要在用戶態(User Mode)和內核態(KerneMode)中來回切換,所以性能開銷還是很大的。
虛擬線程,是JDK 實現的輕量級線程,可以避免上下文切換帶來的的額外耗費。實現原理其實是JDK不再是每一個線程都一對一的對應一個操作系統的線程了,而是會將多個虛擬線程映射到少量操作系統線程中,通過有效的調度來避免那些上下文切換。

虛擬線程和普通線程的區別
- 虛擬線程總是守護線程。setDaemon(false)方法不能將虛擬線程更改為非守護線程。所以,需要注意的是,當所有啟動的非守護線程都終止時,JVM將終止。這意味著JVM不會等待虛擬線程完成后才退出。
- 即使使用setPriority()方法,虛擬線程始終具有normal的優先級,且不能更改優先級。在虛擬線程上調用此方法沒有效果。
- 虛擬線程是不支持stop()、suspend()或resume()等方法。這些方法在虛擬線程上調用時會拋出UnsupportedOperationException異常。
虛擬線程的使用
在JDK21中創建虛擬線程的方式有以下幾種
- 通過
Thread.startVirtualThread方式
Thread.startVirtualThread(() -> {
System.out.println("hello world I am a VirtualThread");
});
- 通過
Thread.Builder.OfVirtual方式
Thread.Builder.OfVirtual myVirtualThread = Thread.ofVirtual().name("my-virtual-thread");
myVirtualThread.start(() -> {
System.out.println("hello world I am a VirtualThread from Thread.Builder.OfVirtual");
});
- 線程池也支持虛擬線程了,也可以通過
Executors.newVirtualThreadPerTaskExecutor()來創建虛擬線程
try(var executors = Executors.newVirtualThreadPerTaskExecutor()){
IntStream.range(0,100).forEach(i -> executors.execute(() -> {
System.out.println("hello world I am a VirtualThread from Executors.newVirtualThreadPerTaskExecutor(),"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}));
}
但是,官方并不建議虛擬線程和線程池一起用,主要就是不想讓虛擬線程進行池化,因為像所有資源池一樣、線程池旨在共享昂貴的資源,但虛擬線程并不昂貴,因此永遠不需要將它們池化。
實際來對比一下性能
先創建一個簡單的任務
public class TestTask implements Runnable{
public void baseTask(){
IntStream.range(0,100).forEach(i -> {
double a = Math.pow(i,2);
});
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public void run() {
baseTask();
}
}
先用普通線程執行一遍任務,統計耗時
public void testUserTask(){
// 1000個線程,1000個任務,普通線程來執行
ExecutorService executorService = Executors.newFixedThreadPool(1000);
long start = System.currentTimeMillis();
IntStream.range(0,1000).forEach(i -> {
executorService.submit(new TestTask());
});
executorService.shutdown();
try {
executorService.awaitTermination(Long.MAX_VALUE,java.util.concurrent.TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("user thread cost:"+(System.currentTimeMillis()-start));
}
運行結果
user thread cost:608ms
用虛擬線程再執行一遍
public void tesVirtualTask(){
try(var executors = Executors.newVirtualThreadPerTaskExecutor()){
long start = System.currentTimeMillis();
IntStream.range(0,1000).forEach(i -> {
executors.submit(new TestTask());
});
executors.shutdown();
try {
executors.awaitTermination(Long.MAX_VALUE,java.util.concurrent.TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("virtual thread cost:"+(System.currentTimeMillis()-start)+"ms");
}
}
運行結果
virtual thread cost:146ms
608ms縮減至146ms,效果非常顯著!
作者:紀莫
歡迎任何形式的轉載,但請務必注明出處。
限于本人水平,如果文章和代碼有表述不當之處,還請不吝賜教。
歡迎掃描二維碼關注公眾號:Jimoer
文章會同步到公眾號上面,大家一起成長,共同提升技術能力。
聲援博主:如果您覺得文章對您有幫助,可以點擊文章右下角【推薦】一下。
您的鼓勵是博主的最大動力!


多線程中的上下文切換是什么?你覺得什么是線程安全?并行和并發有什么區別?守護線程與普通線程有什么區別?JDK21中的虛擬線程是什么?
浙公網安備 33010602011771號