sendfile實現的零拷貝,I/O發生了2次用戶空間與內核空間的上下文切換,以及3次數據拷貝。其中3次數據拷貝中,包括了2次DMA拷貝和1次CPU拷貝。?
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拷貝。






