1.并行與并發有什麼差別?
并行和并發都是指多個任務同時執行的概念,但是它們之間有着明顯的差別。
- 并行:多個任務在同一時刻同時運作,通常需要使用多個處理器或者多核處理器來實作。例如,一個計算機同時執行多個程式、多個線程或者多個程序時,就是采用并行的方式來處理任務,這樣能夠提高計算機的處理效率。
- 并發:多個任務同時進行,但是這些任務的執行是交替進行的,即一個任務執行一段時間後,再執行另外一個任務。它是通過作業系統的協作排程來實作各個任務的切換,達到看上去同時進行的效果。例如,一個多線程程式中的多個線程就是同時運作的,但是因為 CPU 隻能處理一個線程,是以在任意時刻隻有一個線程在執行,線程之間會通過競争的方式來擷取 CPU 的時間片。
總的來說,雖然并行和并發都是多任務處理的方式,但是并行是采用多核處理器等硬體實作任務同步執行,而并發則是通過作業系統的排程算法來合理地配置設定系統資源,使得多個任務看上去同時執行。
2.說說什麼是程序和線程?
程序和線程是作業系統中的概念,用于描述程式運作時的執行實體。
程序:一個程式在執行過程中的一個執行個體,每個程序都有自己獨立的位址空間,也就是說它們不能直接共享記憶體。程序的特點包括:
- 需要占用獨立的記憶體空間;
- 可以并發地執行多個任務;
- 程序之間需要通過程序間通信(IPC)來交換資料;
線程:程序中的一個執行單元,一個程序中可以包含多個線程,這些線程共享程序的記憶體空間。線程的特點包括:
- 線程共享程序記憶體空間,可以友善、高效地通路變量;
- 同一個程序中的多個線程可以并發地執行多個任務;
- 線程之間切換開銷小,可以實作更細粒度的控制,例如 UI 線程控制界面重新整理,工作線程進行耗時的計算等。
線程相比于程序,線程的建立和銷毀開銷較小,上下文切換開銷也較小,是以線程是實作多任務并發的一種更加輕量級的方式。
3.說說線程有幾種建立方式?
Java中建立線程主要有三種方式:
- 定義Thread類的子類,并重寫該類的run方法
/**
* 繼承Thread-重寫run方法
* Created by BaiLi
*/
public class BaiLiThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.run();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("一鍵三連");
}
}
- 定義Runnable接口的實作類,并重寫該接口的run()方法
/**
* 實作Runnable-重寫run()方法
* Created by BaiLi
*/
public class BaiLiRunnable {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
new Thread(myRunnable).start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("一鍵三連");
}
}
- 定義Callable接口的實作類,并重寫該接口的call()方法,一般配合FutureTask使用
/**
* 實作Callable-重寫call()方法
* Created by BaiLi
*/
public class BaiLiCallable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> ft = new FutureTask<String>(new MyCallable());
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
}
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "一鍵三連";
}
}
4.為什麼調用start()方法時會執行run()方法,那怎麼不直接調用run()方法?
JVM執行start方法,會先建立一個線程,由建立出來的新線程去執行thread的run方法,這才起到多線程的效果。
start()和run()的主要差別如下:
- start方法可以啟動一個新線程,run方法隻是類的一個普通方法而已,如果直接調用run方法,程式中依然隻有主線程這一個線程。
- start方法實作了多線程,而run方法沒有實作多線程。
- start不能被重複調用,而run方法可以。
- start方法中的run代碼可以不執行完,就繼續執行下面的代碼,也就是說進行了線程切換。然而,如果直接調用run方法,就必須等待其代碼全部執行完才能繼續執行下面的代碼。
/**
* Created by BaiLi
*/
public class BaiLiDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println(Thread.currentThread().getName()+":一鍵三連"));
thread.start();
thread.run();
thread.run();
System.out.println(Thread.currentThread().getName()+":一鍵三連 + 分享");
}
}
5.線程有哪些常用的排程方法
import java.time.LocalTime;
/**
* Created by BaiLi
*/
public class WaitDemo {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Thread thread1 = new Thread(() -> {
try {
synchronized (lock) {
System.out.println("線程進入永久等待"+ LocalTime.now());
lock.wait();
System.out.println("線程永久等待喚醒"+ LocalTime.now());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Thread-1");
Thread thread2 = new Thread(() -> {
try {
synchronized (lock) {
System.out.println("線程進入逾時等待"+ LocalTime.now());
lock.wait(5000);
System.out.println("線程逾時等待喚醒"+ LocalTime.now());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Thread-2");
thread1.start();
thread2.start();
Thread.sleep(1000);
synchronized (lock) {
lock.notifyAll();
}
thread1.join();
thread2.join();
}
}
public class YieldDemo extends Thread {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " is running");
Thread.yield(); // 調用 yield 方法,讓出 CPU 執行時間
}
}
public static void main(String[] args) {
YieldDemo demo = new YieldDemo();
Thread t1 = new Thread(demo);
Thread t2 = new Thread(demo);
t1.start();
t2.start();
}
}
/**
* Created by BaiLi
*/
public class InterruptedDemo extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":目前線程中斷狀态_"+isInterrupted());
if(isInterrupted()){
if(!interrupted()){
System.out.println(Thread.currentThread().getName()+":目前線程中斷狀态_"+isInterrupted());
}
}
}
public static void main(String[] args) {
InterruptedDemo interruptedDemo = new InterruptedDemo();
interruptedDemo.start();
interruptedDemo.interrupt();
System.out.println(Thread.currentThread().getName()+":目前線程中斷狀态_"+Thread.interrupted());
}
}
6.線程有幾種狀态?
線程在自身的生命周期中, 并不是固定地處于某個狀态,而是随着代碼的執行在不同的狀态之間進行切換,如下圖:
7.什麼是線程上下文切換?
線程上下文切換指的是在多線程運作時,作業系統從目前正在執行的線程中儲存其上下文(包括目前線程的寄存器、程式指針、棧指針等狀态資訊),并将這些資訊恢複到另一個等待執行的線程中,進而實作線程之間的切換。
8.線程間有哪些通信方式?
線程間通信是指在多線程程式設計中,各個線程之間共享資訊或者協同完成某一任務的過程。常用的線程間通信方式有以下幾種:
- 共享變量:共享變量是指多個線程都可以通路和修改的變量,它們通常是在主線程中建立的。多個線程對同一個共享變量進行讀寫操作時,可能會出現競态條件導緻資料錯誤或程式異常。是以需要使用同步機制比如synchronized、Lock等來保證線程安全
- 管道通信:管道是一種基于檔案描述符的通信機制,形成一個單向通信的資料流管道。它通常用于隻有兩個程序或線程之間的通信。其中一個程序将資料寫入到管道(管道的輸出端口),而另一個程序從管道的輸入端口讀取資料
- 信号量:信号量是一種計數器,用于控制多個線程對資源的通路。當一個線程需要通路資源時,它需要申請擷取信号量,如果信号量的計數器值大于 0,則可以通路資源,否則該線程就會等待。當線程結束通路資源後,需要釋放信号量,并将計數器加1
- 條件變量:條件變量是一種通知機制,用于在多個線程之間傳遞狀态資訊和控制資訊。當某個線程需要等待某個條件變量發生改變時,它可以調用 wait() 方法挂起,并且釋放所占用的鎖。當某個線程滿足條件後,可以調用 notify() 或者 signal() 方法來通知等待該條件變量的線程繼續執行
/**
* 共享變量
* 建立人:百裡
*/
public class BaiLiSharedMemoryDemo {
public static void main(String[] args) {
ArrayList<Integer> integers = new ArrayList<>();
Thread producerThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
synchronized (integers) {
integers.add(i);
System.out.println(Thread.currentThread().getName() + "_Producer:" + i);
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "ProducerThread");
Thread consumeThread = new Thread(() -> {
while (true) {
synchronized (integers) {
if (!integers.isEmpty()) {
Integer integer = integers.remove(0);
System.out.println(Thread.currentThread().getName() + "_Consume:" + integer);
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "ConsumeThread");
producerThread.start();
consumeThread.start();
}
}
/**
* 管道通信模式
* 建立人:百裡
*/
public class BaiLiPipedStreamDemo {
public static void main(String[] args) throws IOException {
//輸出管道
PipedOutputStream pipedOutputStream = new PipedOutputStream();
//輸入管道
PipedInputStream pipedInputStream = new PipedInputStream();
pipedInputStream.connect(pipedOutputStream);
Thread producerThread = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
pipedOutputStream.write(i);
System.out.println(Thread.currentThread().getName() + "_Produce: " + i);
Thread.sleep(2000);
}
pipedOutputStream.close();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}, "ProducerThread");
Thread consumeThread = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
while (true) {
int read = pipedInputStream.read();
if (read != -1) {
System.out.println(Thread.currentThread().getName() + "_Consume: " + read);
} else {
break;
}
Thread.sleep(1000);
}
}
pipedInputStream.close();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}, "ConsumeThread");
producerThread.start();
consumeThread.start();
}
}
/**
* 信号量
* 建立人:百裡
*/
public class BaiLiSemaphoreDemo {
public static void main(String[] args) {
// 執行個體化一個信号量對象,初始值為 0
Semaphore semaphore = new Semaphore(0);
// 建立生産者線程
Thread producerThread = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "_Producer:" + i);
semaphore.release(); // 把信号量的計數器加 1
Thread.sleep(1000); //模拟停頓
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "ProducerThread");
// 建立消費者線程
Thread consumeThread = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
semaphore.acquire(); // 請求占有信号量,如果計數器不為 0,計數器減 1,否則線程阻塞等待
System.out.println(Thread.currentThread().getName() + "_Consume:" + i);
Thread.sleep(1000); //模拟停頓
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "ConsumeThread");
producerThread.start();
consumeThread.start();
}
}
/**
* 條件變量|可重入鎖
* 建立人:百裡
*/
public class BaiLIConditionDemo {
public static void main(String[] args) {
// 執行個體化一個可重入鎖對象
ReentrantLock lock = new ReentrantLock();
// 擷取該鎖對象的條件變量
Condition condition = lock.newCondition();
// 建立生産者線程
Thread producerThread = new Thread(() -> {
try {
lock.lock(); // 擷取鎖對象
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + " produce: " + i);
condition.signal(); // 喚醒處于等待狀态下的消費者線程
condition.await(); // 使目前線程處于等待狀态,并釋放鎖對象
Thread.sleep(1000);
}
condition.signal(); // 避免消費者線程一直等待
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 釋放鎖對象
}
}, "producer");
// 建立消費者線程
Thread consumerThread = new Thread(() -> {
try {
lock.lock(); // 擷取鎖對象
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + " consume: " + i);
condition.signal(); // 喚醒處于等待狀态下的生産者線程
condition.await(); // 使目前線程處于等待狀态,并釋放鎖對象
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 釋放鎖對象
}
}, "consumer");
// 啟動生産者和消費者線程
producerThread.start();
consumerThread.start();
}
}
9.ThreadLocal是什麼?
ThreadLocal也就是線程本地變量。如果你建立了一個ThreadLocal變量,那麼通路這個變量的每個線程都會有這個變量的一個本地拷貝,多個線程操作這個變量的時候,實際是操作自己本地記憶體裡面的變量,進而起到線程隔離的作用,避免了線程安全問題。
ThreadLocal是整個線程的全局變量,不是整個程式的全局變量。
/**
* ThreadLocal
* 建立人:百裡
*/
public class BaiLiThreadLocalDemo {
//建立一個靜态的threadLocal變量,被所有線程共享
static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
System.out.println(threadLocal.get());
threadLocal.set(0);
System.out.println(threadLocal.get());
},"Thread-1");
Thread thread2 = new Thread(() -> {
System.out.println(threadLocal.get());
threadLocal.set(1);
System.out.println(threadLocal.get());
},"Thread-2");
thread1.start();
thread1.join();
thread2.start();
thread2.join();
}
}
10.ThreadLocal怎麼實作?
- ThreadLocal是Java中所提供的線程本地存儲機制,可以利用該機制将資料緩存在某個線程内部,該線程可以在任意時刻、任意方法中擷取緩存的資料
- ThreadLocal底層是通過ThreadLocalMap來實作的,每個Thread對象(注意不是ThreadLocal對象)中都存在一個ThreadLocalMap,Map的key為ThreadLocal對象,Map的value為需要緩存的值
實作方式觀察ThreadLocal的set方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocal.ThreadLocalMap threadLocals = null;
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
11.ThreadLocal記憶體洩露是怎麼回事?
如果線上程池中使用ThreadLocal會造成記憶體洩漏,因為當ThreadLocal對象使用完之後,應該要把設定的key,value,也就是Entry對象進行回收,但線程池中的線程不會回收,而線程對象是通過強引用指向ThreadLocalMap,ThreadLocalMap也是通過強引用指向Entry對象,線程不被回收,Entry對象也就不會被回收,進而出現記憶體洩漏。
解決辦法是在使用了ThreadLocal對象之後,手動調用ThreadLocal的remove方法,手動清除Entry對象。
package communication;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 建立人:百裡
*/
public class BaiLiThreadLocalMemoryLeakDemo {
private static final ThreadLocal<byte[]> threadLocal = new ThreadLocal<byte[]>();
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 100; i++) {
executorService.execute(() -> {
byte[] data = new byte[50240 * 10240];
threadLocal.set(data);
// 不調用 remove 方法,會導緻記憶體洩漏
//threadLocal.remove();
});
}
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.MINUTES);
}
}
12.ThreadLocalMap的結構
ThreadLocalMap雖然被稱為Map,但是其實它是沒有實作Map接口的,不過結構還是和HashMap比較類似的,主要關注的是兩個要素:元素數組和散列方法。
- 元素數組一個table數組,存儲Entry類型的元素,Entry是ThreadLocal弱引用作為key,Object作為value的結構。
private Entry[] table;
- 散列方法就是怎麼把對應的key映射到table數組的相應下标,ThreadLocalMap用的是哈希取餘法,取出key的threadLocalHashCode,然後和table數組長度減一&運算(相當于取餘)
int i = key.threadLocalHashCode & (table.length - 1);
補充一點每建立一個ThreadLocal對象,它就會新增0x61c88647,這個值很特殊,它是斐波那契數也叫黃金分割數。這樣帶來的好處就是hash分布非常均勻。
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
13.ThreadLocalMap怎麼解決Hash沖突的?
我們可能都知道HashMap使用了連結清單來解決沖突,也就是所謂的鍊位址法。
ThreadLocalMap内部使用的是開放位址法來解決 Hash沖突的問題。具體來說,當發生Hash沖突時,ThreadLocalMap會将目前插入的元素從沖突位置開始依次往後周遊,直到找到一個空閑的位置,然後把該元素放在這個空閑位置。這樣即使出現了Hash沖突,不會影響到已經插入的元素,而隻是會影響到新的插入操作。
查找的時候,先根據ThreadLocal對象的hash值找到對應的位置,然後比較該槽位Entry對象中的key是否和get的key一緻,如果不一緻就依次往後查找。
14.ThreadLocalMap擴容機制
ThreadLocalMap 的擴容機制和 HashMap 類似,也是在元素數量達到門檻值(預設為數組長度的 2/3)時進行擴容。具體來說,在 set() 方法中,如果目前元素數量已經達到了門檻值,就會調用 rehash() 方法,rehash()會先去清理過期的Entry,然後還要根據條件判斷size >= threshold - threshold / 4 也就是size >= threshold * 3/4來決定是否需要擴容:
private void rehash() {
//清理過期Entry
expungeStaleEntries();
//擴容
if (size >= threshold - threshold / 4)
resize();
}
//清理過期Entry
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
發現需要擴容時調用resize()方法,resize()方法首先将數組長度翻倍,然後建立一個新的數組newTab。接着周遊舊數組oldTab中的所有元素,散列方法重新計算位置,開放位址解決沖突,然後放到新的newTab,周遊完成之後,oldTab中所有的entry資料都已經放入到newTab中了,然後table引用指向newTab.
需要注意的是,新數組的長度始終是2的整數次幂,并且擴容後新數組的長度始終大于舊數組的長度。這是為了保證哈希函數計算出的位置在新數組中仍然有效。
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
15.ThreadLocal怎麼進行父子線程通信
在Java多線程程式設計中,父子線程之間的資料傳遞和共享問題一直是一個非常重要的議題。如果不處理好資料的傳遞和共享,會導緻多線程程式的性能下降或者出現線程安全問題。ThreadLocal是Java提供的一種解決方案,可以非常好地解決父子線程資料共享和傳遞的問題。
那麼它是如何實作通信的了?在Thread類中存在InheritableThreadLocal變量,簡單的說就是使用InheritableThreadLocal來進行傳遞,當父線程的InheritableThreadLocal不為空時,就會将這個值傳到目前子線程的InheritableThreadLocal。
/**
* ThreadLocal父子線程通信
* 建立人:百裡
*/
public class BaiLiInheritableThreadLocalDemo {
public static void main(String[] args) throws Exception {
ThreadLocal threadLocal = new ThreadLocal<>();
threadLocal.set("threadLocal");
ThreadLocal inheritableThreadLocal = new InheritableThreadLocal();
inheritableThreadLocal.set("分享 + inheritableThreadLocal");
Thread t = new Thread(() -> {
System.out.println("一鍵三連 + " + threadLocal.get());
System.out.println("一鍵三連 + " + inheritableThreadLocal.get());
});
t.start();
}
}
16.說一下你對Java記憶體模型(JMM)的了解?
Java 記憶體模型(Java Memory Model)是一種規範,用于描述 Java 虛拟機(JVM)中多線程情況下,線程之間如何協同工作,如何共享資料,并保證多線程的操作在各個線程之間的可見性、有序性和原子性。
具體定義如下:
- 所有的變量都存儲在主記憶體(Main Memory)中。
- 每個線程都有一個私有的本地記憶體(Local Memory),本地記憶體中存儲了該線程以讀/寫共享變量的拷貝副本。
- 線程對變量的所有操作都必須在本地記憶體中進行,而不能直接讀寫主記憶體。
- 不同的線程之間無法直接通路對方本地記憶體中的變量;線程間共享變量時,通過主記憶體來實作通信、協作和傳遞資訊。
Java記憶體模型的抽象圖:
在這個抽象的記憶體模型中,在兩個線程之間的通信(共享變量狀态變更)時,會進行如下兩個步驟:
- 線程A把在本地記憶體更新後的共享變量副本的值,重新整理到主記憶體中。
- 線程B在使用到該共享變量時,到主記憶體中去讀取線程A更新後的共享變量的值,并更新線程B本地記憶體的值。
17.說說你對原子性、可見性、有序性的了解?
原子性、有序性、可見性是并發程式設計中非常重要的基礎概念,JMM的很多技術都是圍繞着這三大特性展開。
- 原子性:指一個操作是不可分割、不可中斷的,在執行過程中不會受到其他線程的幹擾,要麼全部執行,要麼就全不執行。即使是在多線程的環境下,一個操作也是原子性的執行完成。
線程切換會帶來原子性問題,示例:
int count = 0; //1
count++; //2
int a = count; //3
上面展示語句中,除了語句1是原子操作,其它兩個語句都不是原子性操作,下面我們來分析一下語句2
其實語句2在執行的時候,包含三個指令操作
- 指令 1:首先,把變量 count 從記憶體加載到 CPU 的寄存器
- 指令 2:然後,在寄存器中執行 +1 操作
- 指令 3:最終,将結果寫入記憶體
對于上面的三條指令來說,如果線程 A 在指令 1 執行完後做線程切換,線程 A 和線程 B 按照下圖的序列執行,那麼我們會發現兩個線程都執行了 count+=1 的操作,但是得到的結果不是我們期望的 2,而是 1。
- 可見性:指一個線程對共享變量的修改,對于其他線程應該是立即可見的,確定了各個線程之間對記憶體狀态的正确觀察。
- 有序性:指程式執行的順序按照代碼的順序執行。在單線程的情況下,代碼執行順序與編寫的順序一緻。但在多線程環境中,由于時間片輪換,不同的線程可能會交替執行不同的代碼。
原子性、可見性、有序性都應該怎麼保證呢?
- 原子性:JMM隻能保證基本的原子性,如果要保證一個代碼塊的原子性,需要使用synchronized。
- 可見性:Java是利用volatile關鍵字來保證可見性的,除此之外,final和synchronized也能保證可見性。
- 有序性:synchronized或者volatile都可以保證多線程之間操作的有序性。
18.說說什麼是指令重排?
在不影響單線程程式執行結果的前提下,計算機為了最大限度的發揮機器性能,對機器指令進行重排序優化。
從Java源代碼到最終實際執行的指令序列,會分别經曆下面3種重排序:
- 編譯器重排序:編譯器在不改變單線程程式語義的前提下重新安排語句的執行順序。例如把變量緩存到寄存器中、提取公共子表達式等。
- 指令級重排序:如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。例如亂序執行的 Load 和 Store 指令、分支預測以及指令突發等。
- 記憶體系統重排序:由于資料讀寫過程中涉及到多個緩沖區,這使得加載和存儲的操作看上去可能是亂序執行,于是需要記憶體系統的重排序。例如寫入緩存中的操作順序,對于其他CPU的 Cache 來說是不可見的。
以雙重校驗鎖單例模式為例子,Singleton instance=new Singleton();對應的JVM指令分為三步:配置設定記憶體空間-->初始化對象--->對象指向配置設定的記憶體空間,但是經過了編譯器的指令重排序,第二步和第三步就可能會重排序。
JMM屬于語言級的記憶體模型,它確定在不同的編譯器和不同的處理器平台之上,通過禁止特定類型的編譯器重排序和指令級重排序,為程式員提供一緻的記憶體可見性保證。
19.指令重排有限制嗎?happens-before了解嗎?
指令重排也是有一些限制的,有兩個規則happens-before和as-if-serial來限制。
happens-before的定義:
- 如果一個操作happens-before另一個操作,那麼第一個操作的執行結果将對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。
- 兩個操作之間存在happens-before關系,并不意味着Java平台的具體實作必須要按照 happens-before關系指定的順序來執行。隻要沒有改變程式的執行結果,編譯器和處理器怎麼優化都可以。
happens-before的六大規則:
- 程式順序規則:一個線程中的每個操作,happens-before于該線程中的任意後續操作。
/**
* 順序性規則
* 順序執行是針對代碼邏輯而言的,在實際執行的時候發生指令重排序但是并沒有改變源代碼的邏輯。
* @author 百裡
*/
public class BaiLiHappenBeforeDemo {
public static void main(String[] args) {
double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r; // C
System.out.println(area);
}
}
- 螢幕鎖規則:一個unlock操作之前對某個鎖的lock操作必須發生在該unlock操作之前
import java.util.concurrent.locks.ReentrantLock;
/**
* 重排鎖的話,會導緻邏輯改變。
* @author 百裡
*/
public class BaiLiHappenBeforeLockDemo {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();
// TODO
reentrantLock.unlock();
reentrantLock.lock();
// TODO
reentrantLock.unlock();
}
}
- volatile變量規則:對一個volatile變量的寫操作必須發生在該變量的讀操作之前。
- 傳遞性規則:如果A happens-before B,且B happens-before C,那麼A happens-before C。
從圖中,我們可以看到:
- “x=42”Happens-Before 寫變量“v=true”,這是規則1的内容;
- 寫變量“v=true”Happens-Before 讀變量“v=true”,這是規則3的内容;
- 再根據這個傳遞性規則,我們得到結果:“x=42”Happens-Before 讀變量“v=true”;
這意味着什麼呢?如果線程 B 讀到了“v=true”,那麼線程A設定的“x=42”對線程B是可見的。也就是說,線程B能看到“x == 42“。
/**
* 傳遞性規則
* @author 百裡
*/
public class BaiLiHappenBeforeVolatileDemo {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
System.out.println(x);
}
}
}
- 線程啟動規則:如果線程A執行操作ThreadB.start()(啟動線程B),那麼A線程的ThreadB.start()操作happens-before于線程B中的任意操作。
我們可以了解為:線程A啟動線程B之後,線程B能夠看到線程A在啟動線程B之前的操作。
- 線程結束規則:如果線程A執行操作ThreadB.join()并成功傳回,那麼線程B中的任意操作 happens-before于 ThreadB.join()操作成功傳回後的線程A操作。
在Java語言裡面,Happens-Before的語義本質上是一種可見性,A Happens-Before B 意味着A事件對B事件來說是可見的,并且無論A事件和B事件是否發生在同一個線程裡。
20.as-if-serial又是什麼?單線程的程式一定是順序的嗎?
as-if-serial是指無論如何重排序都不會影響單線程程式的執行結果。這個原則的核心思想是編譯器和處理器等各個層面的優化,不能改變程式執行的意義。
A和C之間存在資料依賴關系,同時B和C之間也存在資料依賴關系。是以在最終執行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程式的結果将會被改變)。但A和B之間沒有資料依賴關系,編譯器和處理器可以重排序A和B之間的執行順序。
是以最終,程式可能會有兩種執行順序:
21.volatile實作原理了解嗎?
volatile有兩個作用,保證可見性和有序性。
可見性:當一個變量被聲明為 volatile 時,它會告訴編譯器和CPU将該變量存儲在主記憶體中,而不是線程的本地記憶體中。即每個線程讀取的都是主記憶體中最新的值,避免了多線程并發下的資料不一緻問題。
有序性:重排序可以分為編譯器重排序和處理器重排序,valatile保證有序性,就是通過分别限制這兩種類型的重排序。
為了實作volatile的記憶體語義,編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障來禁止特定類型的處理器重排序。
- 在每個volatile寫操作的前面插入一個StoreStore屏障
- 在每個volatile寫操作的後面插入一個StoreLoad屏障
- 在每個volatile讀操作的後面插入一個LoadLoad屏障
- 在每個volatile讀操作的後面插入一個LoadStore屏障
22.synchronized用過嗎?怎麼使用?
synchronized經常用的,用來保證代碼的原子性。
synchronized主要有三種用法:
- 修飾執行個體方法: 作用于目前對象執行個體加鎖,進入同步代碼前要獲得目前對象執行個體的鎖。
- 修飾靜态方法:給目前類加鎖,在同一時間内,隻能有一個線程持有該類對應的 Class 對象的鎖,其他線程需要等待鎖的釋放才能繼續執行該靜态方法。
- 修飾代碼塊 :指定一個同步鎖對象,這個對象可以是具體的Object或者是類.class。在同一時間内,隻能有一個線程持有該同步鎖對象的鎖,其他線程需要等待鎖的釋放才能繼續執行該代碼塊。
注意事項:
- 修飾執行個體方法:不同的對象執行個體之間并不會互相影響,它們的鎖是互相獨立的。是以,如果不同的線程在不同的對象執行個體上執行同一個同步方法,它們之間并不會因為共享變量而産生互斥的效果。
- 修飾靜态方法:應該盡量避免持有鎖的時間過長,否則可能會導緻其他線程長時間等待,進而影響系統性能。同時,也要注意避免死鎖的情況。
- 修飾代碼塊:同步鎖并不是對整個代碼塊進行加鎖,而是對同步鎖對象進行加鎖。是以,如果在不同的代碼塊中使用了相同的同步鎖對象,它們之間也會産生互斥的效果。而如果使用不同的同步鎖對象,則它們之間并不會産生互斥的效果。
23.synchronized的實作原理?
我們使用synchronized的時候,發現不用自己去lock和unlock,是因為JVM幫我們把這個事情做了。
- synchronized修飾代碼塊時,JVM采用monitorenter、monitorexit兩個指令來實作同步,monitorenter 指令指向同步代碼塊的開始位置, monitorexit 指令則指向同步代碼塊的結束位置。
/**
* @author 百裡
*/
public class BaiLiSyncDemo {
public void main(String[] args) {
synchronized (this) {
int a = 1;
}
}
}
public void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=5, args_size=2
0: aload_0
1: dup
2: astore_2
3: monitorenter
4: iconst_1
5: istore_3
6: aload_2
7: monitorexit
8: goto 18
11: astore 4
13: aload_2
14: monitorexit
15: aload 4
17: athrow
18: return
- synchronized修飾同步方法時,JVM采用ACC_SYNCHRONIZED标記符來實作同步,這個辨別指明了該方法是一個同步方法。同樣可以寫段代碼反編譯看一下。
/**
* @author 百裡
*/
public class BaiLiSyncDemo {
public synchronized void main(String[] args) {
int a = 1;
}
}
synchronized鎖住的是什麼呢?
執行個體對象結構裡有對象頭,對象頭裡面有一塊結構叫Mark Word,Mark Word指針指向了monitor。
所謂的Monitor其實是一種同步機制,我們可以稱為内部鎖或者Monitor鎖。
monitorenter、monitorexit或者ACC_SYNCHRONIZED都是基于Monitor實作的。
反編譯class檔案方法:
反編譯一段synchronized修飾代碼塊代碼,javap -c -s -v -l ***.class,可以看到相應的位元組碼指令。
24.synchronized的可見性,有序性,可重入性是怎麼實作的?
synchronized怎麼保證可見性?
- 線程加鎖前,将清空工作記憶體中共享變量的值,進而使用共享變量時需要從主記憶體中重新讀取最新的值。
- 線程加鎖後,其它線程無法擷取主記憶體中的共享變量。
- 線程解鎖前,必須把共享變量的最新值重新整理到主記憶體中。
synchronized怎麼保證有序性?
synchronized同步的代碼塊,具有排他性,一次隻能被一個線程擁有,是以synchronized保證同一時刻,代碼是單線程執行的。
因為as-if-serial語義的存在,單線程的程式能保證最終結果是有序的,但是不保證不會指令重排。
是以synchronized保證的有序是執行結果的有序性,而不是防止指令重排的有序性。
synchronized怎麼實作可重入的?
synchronized 是可重入鎖,也就是說,允許一個線程二次請求自己持有對象鎖的臨界資源,這種情況稱為可重入鎖。
之是以是可重入的。是因為 synchronized 鎖對象有個計數器,當一個線程請求成功後,JVM會記下持有鎖的線程,并将計數器計為1。此時其他線程請求該鎖,則必須等待。而該持有鎖的線程如果再次請求這個鎖,就可以再次拿到這個鎖,同時計數器會遞增。
當線程執行完畢後,計數器會遞減,直到計數器為0才釋放該鎖。
25.說說synchronized和ReentrantLock的差別
可以從鎖的實作、性能、功能特點等幾個次元去回答這個問題:
- 鎖的實作: synchronized是Java語言的關鍵字,基于JVM實作。而ReentrantLock是基于JDK的API層面實作的(一般是lock()和unlock()方法配合try/finally語句塊來完成。)
- 性能: 在JDK1.6鎖優化以前,synchronized的性能比ReenTrantLock差很多。但是JDK6開始,增加了适應性自旋、鎖消除等,兩者性能就差不多了。
- 功能特點: ReentrantLock 比 synchronized 多了一些進階功能,如等待可中斷、可實作公平鎖、可實作選擇性通知。
- ReentrantLock提供了一種能夠中斷等待鎖的線程的機制,通過lock.lockInterruptibly()來實作這個機制
- ReentrantLock可以指定是公平鎖還是非公平鎖。而synchronized隻能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。
- synchronized與wait()和notify()/notifyAll()方法結合實作等待/通知機制;ReentrantLock類借助Condition接口與newCondition()方法實作。
- ReentrantLock需要手工聲明來加鎖和釋放鎖,一般跟finally配合釋放鎖。而synchronized不用手動釋放鎖。
下面的表格列出了兩種鎖之間的差別:
26.ReentrantLock實作原理?
ReentrantLock是一種可重入的排它鎖,主要用來解決多線程對共享資源競争的問題;它提供了比synchronized關鍵字更加靈活的鎖機制。其實作原理主要涉及以下三個方面:
- 基本結構
ReentrantLock内部維護了一個Sync對象(AbstractQueuedSynchronizer類的子類),Sync持有鎖、等待隊列等狀态資訊,實際上 ReentrantLock的大部分功能都是由Sync來實作的。
- 加鎖過程
當一個線程調用ReentrantLock的lock()方法時,會先嘗試CAS操作擷取鎖,如果成功則傳回;否則,線程會被放入等待隊列中,等待喚醒重新嘗試擷取鎖。
如果一個線程已經持有了鎖,那麼它可以重入這個鎖,即繼續擷取該鎖而不會被阻塞。ReentrantLock通過維護一個計數器來實作重入鎖功能,每次重入計數器加1,每次釋放鎖計數器減1,當計數器為0時,鎖被釋放。
- 解鎖過程
當一個線程調用ReentrantLock的unlock()方法時,會将計數器減1,如果計數器變為了0,則鎖被完全釋放。如果計數器還大于0,則表示有其他線程正在等待該鎖,此時會喚醒等待隊列中的一個線程來擷取鎖。
總結:
ReentrantLock的實作原理主要是基于CAS操作和等待隊列來實作。它通過Sync對象來維護鎖的狀态,支援重入鎖和公平鎖等特性,提供了比synchronized更加靈活的鎖機制,是Java并發程式設計中常用的同步工具之一。
27.ReentrantLock怎麼實作公平鎖的?
ReentrantLock可以通過構造函數的參數來控制鎖的公平性,如果傳入 true,就表示該鎖是公平的;如果傳入 false,就表示該鎖是不公平的。
new ReentrantLock()構造函數預設建立的是非公平鎖 NonfairSync
同時也可以在建立鎖構造函數中傳入具體參數建立公平鎖 FairSync
FairSync、NonfairSync 代表公平鎖和非公平鎖,兩者都是 ReentrantLock 靜态内部類,隻不過實作不同鎖語義。
非公平鎖和公平鎖的差別:
- 非公平鎖在調用 lock 後,首先就會調用 CAS 進行一次搶鎖,如果這個時候恰巧鎖沒有被占用,那麼直接就擷取到鎖傳回了。
- 非公平鎖在 CAS 失敗後,和公平鎖一樣都會進入到 tryAcquire 方法,在 tryAcquire 方法中,如果發現鎖這個時候被釋放了(state == 0),非公平鎖會直接 CAS 搶鎖,但是公平鎖會判斷等待隊列是否有線程處于等待狀态,如果有則不去搶鎖,乖乖排到後面。
28.什麼是CAS?
CAS叫做CompareAndSwap,比較并交換,主要是通過處理器的指令來保證操作的原子性的。
CAS 操作包含三個參數:共享變量的記憶體位址(V)、預期原值(A)和新值(B),當且僅當記憶體位址 V 的值等于 A 時,才将 V 的值修改為 B;否則,不會執行任何操作。
在多線程場景下,使用 CAS 操作可以確定多個線程同時修改某個變量時,隻有一個線程能夠成功修改。其他線程需要重試或者等待。這樣就避免了傳統鎖機制中的鎖競争和死鎖等問題,提高了系統的并發性能。
29.CAS存在什麼問題?如何解決?
CAS的經典三大問題:
ABA問題
ABA 問題是指一個變量從A變成B,再從B變成A,這樣的操作序列可能會被CAS操作誤判為未被其他線程修改過。例如線程A讀取了某個變量的值為 A,然後被挂起,線程B修改了這個變量的值為B,然後又修改回了A,此時線程A恢複執行,進行CAS操作,此時仍然可以成功,因為此時變量的值還是A。
怎麼解決ABA問題?
- 加版本号
每次修改變量,都在這個變量的版本号上加1,這樣,剛剛A->B->A,雖然A的值沒變,但是它的版本号已經變了,再判斷版本号就會發現此時的A已經被改過了。
比如使用JDK5中的AtomicStampedReference類或JDK8中的LongAdder類。這些原子類型不僅包含資料本身,還包含一個版本号,每次進行操作時都會更新版本号,隻有當版本号和期望值都相等時才能執行更新,這樣可以避免 ABA 問題的影響。
循環性能開銷
自旋CAS,如果一直循環執行,一直不成功,會給CPU帶來非常大的執行開銷。
怎麼解決循環性能開銷問題?
可以使用自适應自旋鎖,即在多次操作失敗後逐漸加長自旋時間或者放棄自旋鎖轉為阻塞鎖;
隻能保證一個變量的原子操作
CAS 保證的是對一個變量執行操作的原子性,如果需要對多個變量進行複合操作,CAS 操作就無法保證整個操作的原子性。
怎麼解決隻能保證一個變量的原子操作問題?
- 可以使用鎖機制來保證整個複合操作的原子性。例如,使用 synchronized 關鍵字或 ReentrantLock 類來保證多個變量的複合操作的原子性。在鎖的作用下,隻有一個線程可以執行複合操作,其他線程需要等待該線程釋放鎖後才能繼續執行。這樣就可以保證整個操作的原子性了。
- 可以使用類似于事務的方式來保證多個變量的複合操作的原子性。例如,使用AtomicReference 類,可以将多個變量封裝到一個對象中,通過 CAS 操作更新整個對象,進而保證多個變量的複合操作的原子性。隻有當整個對象的值和期望值都相等時才會執行更新操作。
30.Java多線程中如何保證i++的結果正确
- 使用 Atomic變量,Java 并發包内提供了一些原子類型,如AtomicInteger、AtomicLong等,這些類提供了一些原子操作方法,可以保證相應的操作的原子性。
這裡使用 AtomicInteger 類來保證 i++ 操作的原子性。
- 使用synchronized,對i++操作加鎖。
這裡使用 synchronized 方法來保證 increment() 方法的原子性,進而保證 i++ 操作的結果正确。
- 使用鎖機制,除了使用 synchronized 關鍵字外,還可以使用 ReentrantLock 類來保護臨界區域。
這裡使用 ReentrantLock 類的 lock() 和 unlock() 方法來保護 i++操作的原子性。
31.AtomicInteger的原理是什麼?
一句話概括:使用CAS實作。
在AtomicInteger中,CAS操作的流程如下:
- 調用 incrementAndGet()方法,該方法會通過調用unsafe.getAndAddInt()方法,擷取目前 AtomicInteger對象的值val
- 将 val + 1 得到新的值 next
- 使用 unsafe.compareAndSwapInt() 方法進行 CAS 操作,将對象中目前值與預期值(步驟1中擷取的val)進行比較,如果相等,則将 next指派給val;否則傳回 false
- 如果CAS操作傳回false,說明有其他線程已經修改了AtomicInteger對象的值,需要重新執行步驟 1
總結:
在 CAS 操作中,由于隻有一個線程可以成功修改共享變量的值,是以可以保證操作的原子性,即多線程同時修改AtomicInteger變量時也不會出現競态條件。這樣就可以在多線程環境下安全地對AtomicInteger進行整型變量操作。其它的原子操作類基本都是大同小異。
32.什麼是線程死鎖?我們該如何避免線程死鎖?
死鎖是指兩個或兩個以上的線程在執行過程中,因争奪資源而造成的互相等待的現象,在無外力作用的情況下,這些線程會一直互相等待而無法繼續運作下去。
那麼為什麼會産生死鎖呢?死鎖的産生必須具備以下四個條件:
- 互斥條件:指線程在占用某個資源時,會把該資源标記為已占用,并且鎖住該資源,以保證同一時間内隻有一個線程可以通路該資源。其他需要通路該資源的線程就隻能等待該線程釋放該資源,在資源被釋放之後才能進行通路。
- 請求并持有條件:指一個線程己經持有了至少一個資源,但又提出了新的資源請求,而新資源己被其它線程占有,是以目前線程會被阻塞,但阻塞的同時并不釋放自己已經擷取的資源。
- 不可剝奪條件:指線程擷取到的資源在自己使用完之前不能被其它線程搶占,隻有在自己使用完畢後才由自己釋放該資源。
- 環路等待條件:指在發生死鎖時,若幹線程形成頭尾相接的循環等待資源關系。
該如何避免死鎖呢?答案是至少破壞死鎖發生的一個條件。
- 互斥:我們沒有辦法破壞,因為用鎖為的就是互斥。不過其他三個條件都是有辦法打破的,到底如何做呢?
- 請求并持有:可以一次性請求所有的資源。
- 不可剝奪:設定逾時時間。已經占用資源的線程進一步申請其他資源時,如果長時間申請不到,逾時釋放已經持有的資源。
- 環路等待:注意加鎖順序,保證每個線程按同樣的順序進行加鎖。
33.如何排查死鎖問題
可以使用jdk自帶的指令行工具排查:
- 使用jps查找運作的Java程序:jps -l
- 使用jstack檢視線程堆棧資訊:jstack -l 程序id
基本就可以看到死鎖的資訊。
還可以利用圖形化工具,比如JConsole(JConsole工具在JDK的bin目錄中)。出現線程死鎖以後,點選JConsole線程面闆的檢測到死鎖按鈕,将會看到線程的死鎖資訊。
示範樣例如下:
package lock;
/**
* @author 百裡
*/
public class BaiLiDeadLock {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread-1擷取了鎖1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread-1嘗試擷取鎖2");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread-2擷取了鎖2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("Thread-2嘗試擷取鎖1");
}
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
1.建立連接配接,找到相應的線程,點選連接配接
2.選擇線程标簽,點選檢測死鎖。檢視死鎖線程資訊
34.什麼是線程池?
線程池是一種用于管理和複用線程的機制,它提供了一種執行大量異步任務的方式,并且可以在多個任務之間合理地配置設定和管理系統資源。
線程池的主要優點包括:
- 改善了資源使用率,降低了線程建立和銷毀的開銷。
- 提高了系統響應速度,因為線程池已經預先建立好了一些線程,可以更加快速地配置設定資源以響應使用者請求。
- 提高了代碼可讀性和可維護性,因為線程池将線程管理和任務執行進行了分離,可以更加友善地對其進行調整和優化。
- 可以設定線程數目上限,避免了缺乏控制的線程建立造成的系統無法承受的負載壓力。
35.簡單說一下線程池的工作流程
用一個通俗的比喻:
有一個銀行營業廳,總共有六個視窗,現在有三個視窗坐着三個營業員小姐姐在營業。小天去辦業務,可能會遇到什麼情況呢?
- 小天發現有空閑的視窗,直接去找小姐姐辦理業務。
- 小天發現沒有空閑的視窗,就在排隊區排隊等。
- 小天發現沒有空閑的視窗,等待區也滿了,經理一看,就讓休息的營業員小姐姐趕緊回來上班,等待區号靠前的趕緊去新視窗辦,小天去排隊區排隊。小姐姐比較辛苦,假如一段時間發現他們可以不用接着營業,經理就讓她們接着休息。
- 小天一看,六個視窗都滿了,等待區也沒位置了。小天就開始投訴急了,要鬧,經理趕緊出來了,經理該怎麼辦呢?
- 我們銀行系統已經癱瘓
- 誰叫你來辦的你找誰去
- 看你比較急,去隊裡加個塞
- 今天沒辦法,不行你看改一天
上面的這個流程幾乎就跟JDK線程池的大緻流程類似。
- 營業中的 3個視窗對應核心線程池數:corePoolSize
- 總的營業視窗數6對應:maximumPoolSize
- 打開的臨時視窗在多少時間内無人辦理則關閉對應:unit
- 排隊區就是等待隊列:workQueue
- 無法辦理的時候銀行給出的解決方法對應:RejectedExecutionHandler
- threadFactory 該參數在JDK中是線程工廠,用來建立線程對象,一般不會動。
是以我們線程池的工作流程也比較好了解了:
- 線程池剛建立時,裡面沒有一個線程。任務隊列是作為參數傳進來的。不過,就算隊列裡面有任務,線程池也不會馬上執行它們。
- 當調用 execute() 方法添加一個任務時,線程池會做如下判斷:
- 如果正在運作的線程數量小于 corePoolSize,那麼馬上建立線程運作這個任務;
- 如果正在運作的線程數量大于或等于 corePoolSize,那麼将這個任務放入隊列;
- 如果這時候隊列滿了,而且正在運作的線程數量小于 maximumPoolSize,那麼還是要建立非核心線程立刻運作這個任務;
- 如果隊列滿了,而且正在運作的線程數量大于或等于 maximumPoolSize,那麼線程池會根據拒絕政策來對應處理。
- 當一個線程完成任務時,它會從隊列中取下一個任務來執行。
- 當一個線程無事可做,超過一定的時間(keepAliveTime)時,線程池會判斷,如果目前運作的線程數大于 corePoolSize,那麼這個線程就被停掉。是以線程池的所有任務完成後,它最終會收縮到 corePoolSize 的大小。
36.線程池主要參數有哪些?
package pool;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author 百裡
*/
public class BaiLiThreadPoolDemo {
public static void main(String[] args) {
//基于Executor架構實作線程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
5, //corePoolSize
12, //maximumPoolSize
5, //keepAliveTime
TimeUnit.SECONDS, //unit
new ArrayBlockingQueue<>(5), //workQueue
Executors.defaultThreadFactory(), //threadFactory
new ThreadPoolExecutor.DiscardPolicy() //handler
);
threadPoolExecutor.execute(() -> {
System.out.println(
Thread.currentThread().getName() + ":點贊評論加分享"
);
});
}
}
線程池有七大參數,我們重點關注corePoolSize、maximumPoolSize、workQueue、handler 可以幫助我們更好地了解和優化線程池的性能
- corePoolSize
此值是用來初始化線程池中核心線程數,當線程池中線程數< corePoolSize時,系統預設是添加一個任務才建立一個線程池。當線程數 = corePoolSize時,新任務會追加到workQueue中。
- maximumPoolSize
maximumPoolSize表示允許的最大線程數 = (非核心線程數+核心線程數),當BlockingQueue也滿了,但線程池中總線程數 < maximumPoolSize時候就會再次建立新的線程。
- keepAliveTime
非核心線程 =(maximumPoolSize - corePoolSize ) ,非核心線程閑置下來不幹活最多存活時間。
- unit
線程池中非核心線程保持存活的時間的機關
- TimeUnit.DAYS;天
- TimeUnit.HOURS;小時
- TimeUnit.MINUTES;分鐘
- TimeUnit.SECONDS;秒
- TimeUnit.MILLISECONDS; 毫秒
- TimeUnit.MICROSECONDS; 微秒
- TimeUnit.NANOSECONDS; 納秒
- workQueue
線程池等待隊列,維護着等待執行的Runnable對象。當運作當線程數= corePoolSize時,新的任務會被添加到workQueue中,如果workQueue也滿了則嘗試用非核心線程執行任務,等待隊列應該盡量用有界的。
- threadFactory
建立一個新線程時使用的工廠,可以用來設定線程名、是否為daemon線程等等。
- handler
corePoolSize、workQueue、maximumPoolSize都不可用的時候執行的飽和政策。
37.線程池的拒絕政策有哪些?
線上程池中,當送出的任務數量超過了線程池的最大容量,線程池就需要使用拒絕政策來處理無法處理的新任務。Java 中提供了 4 種預設的拒絕政策:
- AbortPolicy(預設政策):直接抛出 runtime 異常,阻止系統正常運作。
- CallerRunsPolicy:由送出該任務的線程來執行這個任務。
- DiscardPolicy:直接丢棄任務,不給予任何處理。
- DiscardOldestPolicy:丢棄隊列中最老的一個請求,嘗試再次送出目前任務。
除了這些預設的政策之外,我們也可以自定義自己的拒絕政策,實作RejectedExecutionHandler接口即可。
public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 自定義的拒絕政策處理邏輯
}
}
38.線程池有哪幾種工作隊列
- 有界隊列(ArrayBlockingQueue):是一個用數組實作的有界阻塞隊列,按FIFO排序。
- 無界隊列(LinkedBlockingQueue):是基于連結清單結構的阻塞隊列,按FIFO排序,容量可以選擇進行設定,不設定的話,将是一個無邊界的阻塞隊列,是以在任務數量很大且任務執行時間較長時,無界隊列可以保證任務不會被丢棄,但同時也會導緻線程池中線程數量不斷增加,可能會造成記憶體溢出等問題。
- 延遲隊列(DelayQueue):是一個任務定時周期的延遲執行的隊列。根據指定的執行時間從小到大排序,否則根據插入到隊列的先後排序。
- 優先級隊列(PriorityBlockingQueue):是具有優先級的無界阻塞隊列。與無界隊列類似,優先級隊列可以保證所有任務都會被執行,但不同的是優先級隊列可以對任務進行管理和排序,確定高優先級的任務優先執行。
- 同步隊列(SynchronousQueue):是一個不存儲元素的阻塞隊列,每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處于阻塞狀态,吞吐量通常要高于無界隊列。
39.線程池送出execute和submit有什麼差別?
在Java中,線程池中一般有兩種方法來送出任務:execute() 和 submit()
- execute() 用于送出不需要傳回值的任務
- submit() 用于送出需要傳回值的任務。線程池會傳回一個future類型的對象,通過這個 future對象可以判斷任務是否執行成功,并且可以通過future的get()方法來擷取傳回值
40.怎麼關閉線程池?
可以通過調用線程池的shutdown或shutdownNow方法來關閉線程池。它們的原理是周遊線程池中的工作線程,然後逐個調用線程的interrupt方法來中斷線程,是以無法響應中斷的任務可能永遠無法終止。
shutdown:将線程池狀态置為shutdown,并不會立即停止:
- 停止接收外部submit的任務
- 内部正在跑的任務和隊列裡等待的任務,會執行完
- 等到第二步完成後,才真正停止
shutdownNow:将線程池狀态置為stop。一般會立即停止,事實上不一定:
- 和shutdown()一樣,先停止接收外部送出的任務
- 忽略隊列裡等待的任務
- 嘗試将正在跑的任務interrupt中斷
- 傳回未執行的任務清單
shutdown 和shutdownnow差別如下:
- shutdownNow:能立即停止線程池,正在跑的和正在等待的任務都停下了。這樣做立即生效,但是風險也比較大。
- shutdown:隻是關閉了送出通道,用submit()是無效的;而内部的任務該怎麼跑還是怎麼跑,跑完再徹底停止線程池。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author 百裡
*/
public class BaiLiShutdownDemo {
public static void main(String[] args) {
// 建立一個線程池,包含兩個線程
ExecutorService executor = Executors.newFixedThreadPool(2);
// 送出任務到線程池
executor.submit(() -> {
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Task 1 finished");
});
executor.submit(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Task 2 finished");
});
// 關閉線程池
executor.shutdown();
while (!executor.isTerminated()) {
System.out.println("Waiting for all tasks to finish...");
try {
// 每500毫秒檢查一次
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("All tasks finished");
}
}
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* @author 百裡
*/
public class BaiLiShutdownNowDemo {
public static void main(String[] args) {
// 建立一個線程池,包含兩個線程
ExecutorService executor = Executors.newFixedThreadPool(2);
// 送出任務到線程池
executor.submit(() -> {
while (!Thread.interrupted()) {
System.out.println("Task 1 running...");
}
System.out.println("Task 1 interrupted");
});
executor.submit(() -> {
while (!Thread.interrupted()) {
System.out.println("Task 2 running...");
}
System.out.println("Task 2 interrupted");
});
// 關閉線程池
List<Runnable> unfinishedTasks = null;
executor.shutdownNow();
try {
// 等待直到所有任務完成或逾時60秒
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
// 如果等待逾時,則記錄未完成的任務清單
unfinishedTasks = executor.shutdownNow();
System.out.println("Some tasks were not finished");
}
} catch (InterruptedException e) {
// 如果等待過程中發生異常,則記錄未完成的任務清單
unfinishedTasks = executor.shutdownNow();
Thread.currentThread().interrupt();
}
if (unfinishedTasks != null && !unfinishedTasks.isEmpty()) {
System.out.println("Unfinished tasks: " + unfinishedTasks);
} else {
System.out.println("All tasks finished");
}
}
}
41.有哪幾種常見的線程池
在Java中,常見的線程池類型主要有四種,都是通過工具類Excutors建立出來的。
- newFixedThreadPool (固定數目線程):該線程池具有固定的線程數,當送出的任務超過線程池大小時,會将任務放入隊列等待執行
- newCachedThreadPool (可緩存線程):該線程池的線程數不定,當線程池中有空閑線程時,會直接使用空閑線程,否則會建立新的線程執行任務。适用于執行大量短生命周期的異步任務。
- newSingleThreadExecutor (單線程):該線程池隻有一個線程,在該線程執行任務的過程中,其他任務都會在隊列中等待執行。
- newScheduledThreadPool (定時及周期執行):該線程池可以執行定時任務和周期性任務,也可以送出普通的異步任務。
需要注意阿裡巴巴《Java開發手冊》裡禁止使用這種方式來建立線程池。
42.說一說newSingleThreadExecutor工作原理
線程池特點:
- 核心線程數為1
- 最大線程數也為1
- 阻塞隊列是無界隊列LinkedBlockingQueue,可能會導緻OOM
- keepAliveTime為0
工作流程:
- 送出任務
- 線程池是否有一個線程正在運作,如果沒有,建立線程執行任務
- 如果有并且非空閑狀态,将任務加到阻塞隊列
- 目前的唯一線程,從隊列取任務,執行完一個,再繼續取,一個線程執行任務。
使用場景:
适用于串行執行任務的場景,一個任務一個任務地執行。
43.說一說newFixedThreadPool工作原理
線程池特點:
- 核心線程數和最大線程數大小一樣
- 沒有所謂的非空閑時間,即keepAliveTime為0
- 阻塞隊列為無界隊列LinkedBlockingQueue,可能會導緻OOM
工作流程:
- 送出任務
- 如果線程數少于核心線程,建立核心線程執行任務
- 如果線程數等于核心線程,把任務添加到LinkedBlockingQueue阻塞隊列
- 如果線程執行完任務,去阻塞隊列取任務,繼續執行。
使用場景:
FixedThreadPool 适用于處理CPU密集型的任務,確定CPU在長期被工作線程使用的情況下,盡可能的少的配置設定線程,即适用執行長期的任務。
44.說一說newCachedThreadPool工作原理
線程池特點:
- 核心線程數為0
- 最大線程數為Integer.MAX_VALUE,即無限大,可能會因為無限建立線程,導緻OOM
- 阻塞隊列是SynchronousQueue
- 非核心線程空閑存活時間為60秒
當送出任務的速度大于處理任務的速度時,每次送出一個任務,就必然會建立一個線程。極端情況下會建立過多的線程,耗盡 CPU 和記憶體資源。由于空閑 60 秒的線程會被終止,長時間保持空閑的 CachedThreadPool 不會占用任何資源。
工作流程:
- 送出任務
- 因為沒有核心線程,是以任務直接加到SynchronousQueue隊列。
- 判斷是否有空閑線程,如果有,就去取出任務執行。
- 如果沒有空閑線程,就建立一個線程執行。
- 執行完任務的線程,還可以存活60秒,如果在這期間,接到任務,可以繼續活下去;否則,被銷毀。
使用場景:
用于并發執行大量短期的小任務。
45.說一說newScheduledThreadPool工作原理
線程池特點:
- 最大線程數為Integer.MAX_VALUE,也有OOM的風險
- 阻塞隊列是DelayedWorkQueue
- keepAliveTime為0
- scheduleAtFixedRate() :按某種速率周期執行
- scheduleWithFixedDelay():在某個延遲後執行
工作流程:
- 線程從DelayQueue中擷取已到期的ScheduledFutureTask(DelayQueue.take())。到期任務是指ScheduledFutureTask的time大于等于目前時間。
- 線程執行這個ScheduledFutureTask。
- 線程修改ScheduledFutureTask的time變量為下次将要被執行的時間。
- 線程把這個修改time之後的ScheduledFutureTask放回DelayQueue中(DelayQueue.add())。
使用場景:
周期性執行任務的場景,需要限制線程數量的場景。
import java.util.concurrent.*;
/**
* @author 百裡
*/
public class BaiLiScheduledThreadPoolDemo {
public static void main(String[] args) throws Exception {
// 建立一個可以執行定時任務的線程池
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
// 排程一個定時任務,每隔 2 秒鐘輸出一次目前時間
ScheduledFuture<?> scheduledFuture = executorService.scheduleAtFixedRate(() -> {
System.out.println("Current time: " + System.currentTimeMillis());
}, 0, 2, TimeUnit.SECONDS);
// 主線程休眠 10 秒鐘後取消任務
Thread.sleep(10000);
scheduledFuture.cancel(true);
// 關閉線程池
executorService.shutdown();
}
}
import java.util.concurrent.*;
/**
* @author 百裡
*/
public class BaiLiScheduleWithFixedDelayDemo {
public static void main(String[] args) throws Exception {
// 建立一個可以執行定時任務的線程池
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
// 排程一個周期性任務,每次任務執行完畢後等待 2 秒鐘再執行下一個任務
executorService.scheduleWithFixedDelay(() -> {
System.out.println("Current time: " + System.currentTimeMillis());
}, 0, 2, TimeUnit.SECONDS);
// 主線程休眠 10 秒鐘後關閉線程池
Thread.sleep(10000);
executorService.shutdown();
}
}
46.線程池異常怎麼處理知道嗎?
在使用線程池處理任務的時候,任務代碼可能抛出RuntimeException,抛出異常後,線程池可能捕獲它,也可能建立一個新的線程來代替異常的線程,我們可能無法感覺任務出現了異常,是以我們需要考慮線程池異常情況。
常見的異常處理方式:
1.try-catch捕獲異常
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author 百裡
*/
public class BaiLiHandlerException implements Runnable {
@Override
public void run() {
try {
// 任務代碼
int a = 10 / 0;
} catch (Exception e) {
System.err.println("任務執行異常:" + e.getMessage());
}
}
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
BaiLiHandlerException task = new BaiLiHandlerException();
executor.execute(task);
}
}
2.使用Thread.UncaughtExceptionHandler處理異常
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.*;
/**
* @author 百裡
*/
public class BaiLiHandlerException implements Runnable {
@Override
public void run() {
// 任務代碼
int a = 10 / 0;
}
public static class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.err.println("任務執行異常:" + e.getMessage());
}
}
public static void main(String[] args) {
BaiLiHandlerException task = new BaiLiHandlerException();
Thread thread = new Thread(task);
thread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
thread.start();
}
}
3.重寫ThreadPoolExecutor.afterExcute處理異常
package exception;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.*;
/**
* @author 百裡
*/
public class BaiLiHandlerException implements Runnable {
@Override
public void run() {
// 任務代碼
int a = 10 / 0;
}
public static class MyThreadPoolExecutor extends ThreadPoolExecutor {
public MyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if (t != null) {
System.err.println("任務執行異常:" + t.getMessage());
}
}
}
public static void main(String[] args) {
MyThreadPoolExecutor executor = new MyThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(), new ThreadFactoryBuilder().setNameFormat("MyThread-%d").build());
BaiLiHandlerException task = new BaiLiHandlerException();
executor.execute(task);
}
}
4.使用future.get處理異常
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.*;
/**
* @author 百裡
*/
public class BaiLiHandlerException {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
throw new RuntimeException("任務執行失敗");
});
try {
String result = future.get();
System.out.println(result);
} catch (ExecutionException e) {
Throwable ex = e.getCause();
System.out.println("捕獲到異常: " + ex.getMessage());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
executor.shutdownNow();
System.out.println("線程被中斷,已執行相應處理");
}
executor.shutdown();
}
}
47.能說一下線程池有幾種狀态嗎?
線程池有這幾個狀态:RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED
//線程池狀态
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
線程池各個狀态切換圖:
RUNNING
- 該狀态的線程池會接收新任務,并處理阻塞隊列中的任務;
- 調用線程池的shutdown()方法,可以切換到SHUTDOWN狀态;
- 調用線程池的shutdownNow()方法,可以切換到STOP狀态;
SHUTDOWN
- 該狀态的線程池不會接收新任務,但會處理阻塞隊列中的任務;
- 隊列為空,并且線程池中執行的任務也為空,進入TIDYING狀态;
STOP
- 該狀态的線程不會接收新任務,也不會處理阻塞隊列中的任務,而且會中斷正在運作的任務;
- 線程池中執行的任務為空,進入TIDYING狀态;
TIDYING
- 該狀态表明所有的任務已經運作終止,記錄的任務數量為0。
- terminated()執行完畢,進入TERMINATED狀态
TERMINATED
- 該狀态表示線程池徹底終止
48.單機線程池執行斷電了應該怎麼處理?
單機線程池是一種常見的多線程程式設計方式,它可以用于異步執行任務,提高應用程式的性能和并發能力。在單機線程池中,所有任務都由同一個線程處理,是以如果該線程在執行任務時突然斷電,則會出現以下問題:
- 任務不完整
- 資料丢失
- 系統資源洩漏
如果單機線程池在執行任務時突然遇到斷電等異常情況,應該盡快采取以下措施:
- 恢複中斷的任務:當系統重新啟動後,需要檢查之前被中斷的任務,并将其重新送出到單機線程池中進行處理,以確定所有任務都能夠被正确執行。
- 資料備份及恢複:對于需要處理的資料,應該事先進行備份,以避免資料丢失或損壞。在系統重新開機後,需要将備份資料恢複到系統中,以確定資料的完整性和正确性。
- 系統資源清理:在單機線程池執行過程中,可能會産生未釋放的系統資源,如檔案句柄、鎖等。在系統重新開機後,需要清理這些未釋放的系統資源,以避免資源洩漏和系統運作不穩定。
49.NIO的原理,包括哪幾個元件?
NIO(Java Non-blocking I/O)是一種 I/O 技術,其核心原理是基于事件驅動的方式進行操作。
NIO 的工作原理:基于緩沖區、通道和選擇器的組合,通過高效地利用系統資源,以支援高并發和高吞吐量的資料處理。相比傳統的 I/O 程式設計方式,Java NIO 提供了更為靈活和高效的程式設計方式。
NIO三大核心元件: Channel(通道)、Buffer(緩沖區)、Selector(選擇器)。
Selector、Channel 和 Buffer 的關系圖如下:
- Channel(通道):類似于傳統 I/O 中的 Stream,是用于實際資料傳輸的元件。在 NIO 中,有多種類型的 Channel 可以使用,例如 FileChannel、SocketChannel、DatagramChannel 等,可用于檔案操作、網絡傳輸等不同場景。
- Buffer(緩沖區):用于存儲資料的容器,可以了解為暫存需要傳輸的資料的地方。在 NIO 中,存在多種類型的緩沖區,如 ByteBuffer、CharBuffer、IntBuffer等。
- Selector(選擇器):用于注冊 Channel 并監聽其 I/O 事件。當 Channel 準備好讀或寫資料時,會得到通知。Selector 可以高效地輪詢多個 Channel,并且避免了使用多線程或多程序對多個 Channel 進行輪詢的情況,進而減少了系統資源開銷。
通俗了解NIO原理:
NIO 是可以做到用一個線程來處理多個操作的。假設有 10000 個請求過來,根據實際情況,可以配置設定 50 或者 100 個線程來處理。不像之前的阻塞 IO 那樣,非得配置設定 10000 個。
50.什麼是零拷貝?
零拷貝(Zero-Copy)是一種 I/O 操作優化技術,可以快速高效地将資料從檔案系統移動到網絡接口,而不需要将其從核心空間複制到使用者空間。
傳統I/O操作過程:
傳統 I/O 的工作方式是,資料讀取和寫入是從使用者空間到核心空間來回複制,而核心空間的資料是通過作業系統層面的 I/O 接口從磁盤讀取或寫入。代碼通常如下,一般會需要兩個系統調用:
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
代碼很簡單,雖然就兩行代碼,但是這裡面發生了不少的事情:
- 使用者應用程序調用read函數,向作業系統發起IO調用,上下文從使用者态轉為核心态(切換1)
- DMA控制器把資料從磁盤中,讀取到核心緩沖區。
- CPU把核心緩沖區資料,拷貝到使用者應用緩沖區,上下文從核心态轉為使用者态(切換2),read函數傳回
- 使用者應用程序通過write函數,發起IO調用,上下文從使用者态轉為核心态(切換3)
- CPU将應用緩沖區中的資料,拷貝到socket緩沖區
- DMA控制器把資料從socket緩沖區,拷貝到網卡裝置,上下文從核心态切換回使用者态(切換4),write函數傳回
從流程圖可以看出,傳統IO的讀寫流程,包括了4次上下文切換(4次使用者态和核心态的切換),4次資料拷貝(兩次CPU拷貝以及兩次的DMA拷貝)
這種簡單又傳統的檔案傳輸方式,存在備援的上文切換和資料拷貝,在高并發系統裡是非常糟糕的,多了很多不必要的開銷,會嚴重影響系統性能。
是以,要想提高檔案傳輸的性能,就需要減少「使用者态與核心态的上下文切換」和「記憶體拷貝」的次數。
零拷貝主要是用來解決作業系統在處理 I/O 操作時,頻繁複制資料的問題。關于零拷貝主要技術有MMap+Write、SendFile等幾種方式。
Mmap+Wirte實作零拷貝:
- 使用者程序通過mmap方法向作業系統核心發起IO調用,上下文從使用者态切換為核心态。
- CPU利用DMA控制器,把資料從硬碟中拷貝到核心緩沖區。
- 上下文從核心态切換回使用者态,mmap方法傳回。
- 使用者程序通過write方法向作業系統核心發起IO調用,上下文從使用者态切換為核心态。
- CPU将核心緩沖區的資料拷貝到的socket緩沖區。
- CPU利用DMA控制器,把資料從socket緩沖區拷貝到網卡,上下文從核心态切換回使用者态,write調用傳回。
可以發現,mmap+write實作的零拷貝,I/O發生了4次使用者空間與核心空間的上下文切換,以及3次資料拷貝。其中3次資料拷貝中,包括了2次DMA拷貝和1次CPU拷貝。
SendFile實作零拷貝:
- 使用者程序發起sendfile系統調用,上下文(切換1)從使用者态轉向核心态
- DMA控制器,把資料從硬碟中拷貝到核心緩沖區。
- CPU将讀緩沖區中資料拷貝到socket緩沖區
- DMA控制器,異步把資料從socket緩沖區拷貝到網卡,
- 上下文(切換2)從核心态切換回使用者态,sendfile調用傳回。
可以發現,sendfile實作的零拷貝,I/O發生了2次使用者空間與核心空間的上下文切換,以及3次資料拷貝。其中3次資料拷貝中,包括了2次DMA拷貝和1次CPU拷貝。