面試官:什么是多線程中的上下文切換?
在多線程編程中,“上下文切換”指的是操作系統(tǒng)在不同線程之間切換執(zhí)行時保存和恢復(fù)線程狀態(tài)的過程。每個線程都包含一個“上下文”,即當(dāng)前執(zhí)行的狀態(tài)信息,包括寄存器的值、程序計數(shù)器(指令位置)、堆棧指針等。
步驟:
- 保存當(dāng)前線程的狀態(tài):當(dāng)一個線程被掛起時,操作系統(tǒng)會將該線程的寄存器、程序計數(shù)器等狀態(tài)信息保存到內(nèi)存中,以便將來能夠恢復(fù)。
- 恢復(fù)新線程的狀態(tài):接下來,操作系統(tǒng)加載即將運行的線程的狀態(tài)信息,使得該線程能夠從中斷的位置繼續(xù)執(zhí)行。
- 切換到新線程執(zhí)行:完成狀態(tài)的保存和恢復(fù)后,CPU就會開始執(zhí)行新線程的指令。
開銷:
雖然上下文切換使得多個線程能夠共享CPU資源,但它并非完全沒有成本。保存和恢復(fù)狀態(tài)需要時間,頻繁的上下文切換會導(dǎo)致:
- 性能下降:頻繁切換會占用大量的CPU時間,導(dǎo)致真正執(zhí)行任務(wù)的時間減少。
- 緩存失效:每次切換線程時,CPU緩存可能會被刷新,導(dǎo)致緩存效率降低,增加內(nèi)存訪問延遲。
如何減少上下文切換策略?
- 減少線程數(shù)量:
每個線程都需要上下文切換資源,線程越多,切換頻率越高。如果任務(wù)量不大,減少線程數(shù)量可以降低上下文切換的頻率。
import java.util.List;
import java.util.Arrays;
public class ReduceThreadsExample {
public static void main(String[] args) {
List<String> tasks = Arrays.asList("task1", "task2", "task3", /* ... */ "task100");
int threadCount = 10;
int taskPerThread = tasks.size() / threadCount;
for (int i = 0; i < threadCount; i++) {
final int start = i * taskPerThread;
final int end = (i == threadCount - 1) ? tasks.size() : (i + 1) * taskPerThread;
new Thread(() -> {
for (int j = start; j < end; j++) {
System.out.println("Processing " + tasks.get(j));
}
}).start();
}
}
}
- 使用多線程池:
線程池可以復(fù)用線程,避免頻繁創(chuàng)建和銷毀線程,從而減少上下文切換開銷。線程池中的線程會被重新分配任務(wù),避免重復(fù)的上下文切換。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
int task = i;
executor.submit(() -> System.out.println("Processing task " + task));
}
executor.shutdown();
}
}
- 增加任務(wù)的批處理:
盡量將多個小任務(wù)合并為一個批處理任務(wù),減少線程之間的切換。這樣可以在同一個線程中連續(xù)完成多個任務(wù),降低切換的頻率。
import java.util.List;
import java.util.Arrays;
public class BatchProcessingExample {
public static void main(String[] args) {
List<String> tasks = Arrays.asList("task1", "task2", "task3", /* ... */ "task100");
int batchSize = 10;
for (int i = 0; i < tasks.size(); i += batchSize) {
final List<String> batch = tasks.subList(i, Math.min(i + batchSize, tasks.size()));
new Thread(() -> {
for (String task : batch) {
System.out.println("Processing " + task);
}
}).start();
}
}
}
- 盡量減少鎖競爭:
當(dāng)多個線程競爭同一個鎖時,線程會頻繁等待和喚醒,導(dǎo)致頻繁的上下文切換??梢酝ㄟ^優(yōu)化鎖的使用,或者采用更細(xì)粒度的鎖來減少鎖的競爭。例如,使用讀寫鎖來避免多個讀取線程間的競爭。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockCompetitionExample {
private static int counter = 0;
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Counter: " + counter);
}
private static void increment() {
lock.lock();
try {
counter++;
} finally {
lock.unlock();
}
}
}
- 無鎖編程:
在適用的場景下,使用無鎖編程(如原子操作或CAS操作)來實現(xiàn)線程間的同步,避免因為鎖競爭而產(chǎn)生的上下文切換。這通常適用于輕量級的并發(fā)操作。
import java.util.concurrent.atomic.AtomicInteger;
public class LockFreeExample {
private static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Counter: " + counter.get());
}
}
- 使用協(xié)程替代多線程:
協(xié)程是一種輕量級的“線程”實現(xiàn),可以在一個線程中實現(xiàn)多任務(wù)的協(xié)作切換。由于協(xié)程的切換是由程序控制的,不需要操作系統(tǒng)參與,因此可以大幅減少上下文切換的開銷。JDK21后支持協(xié)程。
public class VirtualThreadExample {
public static void main(String[] args) {
try (var executor = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100; i++) {
int taskNumber = i;
executor.submit(() -> {
System.out.println("Processing task " + taskNumber + " in " + Thread.currentThread());
// 模擬一些I/O操作或其他阻塞任務(wù)
try {
Thread.sleep(1000); // 模擬任務(wù)執(zhí)行時間
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return taskNumber;
});
}
} // 自動關(guān)閉 executor,確保所有虛擬線程完成執(zhí)行
}
}