Java并發編程
- 一、 基礎概念
- 1. 進程與線程的區別是什么?
- 2. 創建線程的幾種方式?
- 3. 線程的生命周期(狀態)有哪些?
- 4. 什么是守護線程(Daemon Thread)?
- 5. 線程優先級(Priority)的作用及問題?
- 二、線程安全與鎖機制
- 1. 什么是線程安全?如何實現線程安全?
- 2. synchronized 關鍵字的底層實現原理(對象頭、Monitor 機制)?
- 3. volatile 關鍵字的作用和實現原理(內存屏障、可見性、禁止指令重排)?
- 4. 什么是 CAS(Compare-And-Swap)?其優缺點?
- 5. synchronized 和 ReentrantLock 的區別?
- 6. 可重入鎖(ReentrantLock)的實現原理?
- 7. 公平鎖與非公平鎖的區別?
- 8. 什么是死鎖?如何避免或檢測死鎖?
- 9. 偏向鎖、輕量級鎖、重量級鎖的升級過程?
- 10. 鎖消除(Lock Elimination)和鎖粗化(Lock Coarsening)的原理?
- 三、線程協作與通信
- 1. `wait()`、`notify()`、`notifyAll()` 的使用場景和注意事項?
- 2. `sleep()` 和 `wait()` 的區別?
- 3. 如何實現線程間通信(如生產者-消費者模型)?
- 4. `Condition` 接口的作用及與 `wait/notify` 的區別?
- 四、并發工具類(JUC)
- 1. `CountDownLatch`、`CyclicBarrier`、`Semaphore` 的使用場景和區別
- **1.1 `CountDownLatch`**
- **1.2 `CyclicBarrier`**
- **1.3 `Semaphore`**
- 2. `Exchanger` 的作用
- 3. `Phaser` 的使用場景
- 4. `Future` 和 `CompletableFuture` 的區別
- 5. `StampedLock` 的優化點及使用場景
- **優化點**
- **使用場景**
- 五、 線程池相關問題解析
- 1. 線程池的核心參數及作用
- 2. 線程池的工作流程(任務提交后的處理邏輯)
- 3. 常見的線程池類型及其問題
- 4. 線程池的拒絕策略
- 5. 如何合理配置線程池參數
- 6. 線程池中線程復用(Thread Reuse)的原理
- 示例代碼
- 六、 高級并發特性解析
- 1. Java 內存模型(JMM)的核心概念
- 2. 原子性、可見性、有序性及其保證方法
- 3. 指令重排序及其避免方法
- 4. ThreadLocal 的原理、使用場景及內存泄漏問題
- 5. Fork/Join 框架與工作竊取機制
- 示例代碼
- 七、 并發容器解析
- 1. ConcurrentHashMap 的實現原理(JDK7 vs JDK8)
- 2. CopyOnWriteArrayList 的適用場景及優缺點
- 3. BlockingQueue 的實現類及使用場景
- 4. ConcurrentLinkedQueue 的無鎖實現原理
- 八、 并發設計模式與實戰
- 1. 生產者-消費者模式的實現方式
- 2. 如何實現線程安全的單例模式
- 3. 如何排查和解決死鎖問題
- 排查方法
- 解決措施
- 4. 如何避免競態條件(Race Condition)
- 加鎖同步
- 原子操作
- 不可變對象設計
- 線程安全的集合
- 5. 如何設計高并發場景下的計數器
- 原子變量
- LongAdder/LongAccumulator
- 分段計數器
- 九、底層原理與擴展
- 1. Java 線程與操作系統線程的關系
- 2. 什么是協程(Coroutine)?Java 中的虛擬線程(Loom 項目)
- 3. 如何通過 jstack 分析線程狀態
- 4. 什么是偽共享(False Sharing)?如何避免?
- 十、其他擴展問題
- 1. 如何實現異步編程(如 CompletableFuture、Reactive Streams)?
- CompletableFuture
- Reactive Streams
- 2. 分布式鎖與單機鎖的區別
- **單機鎖**
- **分布式鎖**
- 3. 無鎖編程(Lock-Free)的實現思路
- **CAS(Compare-And-Swap)**
- **樂觀鎖**
- **無鎖數據結構**
- 4. 高并發場景下常見的性能優化手段
- **1. 減少鎖競爭**
- **2. 線程池優化**
- **3. 緩存機制**
- **4. 異步 & 批量處理**
- **5. 數據結構優化**
一、 基礎概念
1. 進程與線程的區別是什么?
進程與線程的主要區別如下:
- 定義
- 進程(Process)是操作系統分配資源的基本單位。
- 線程(Thread)是 CPU 調度的基本單位。
- 資源分配
- 進程擁有獨立的內存空間和系統資源。
- 線程共享進程的資源,如內存和文件句柄。
- 通信方式
- 進程間通信(IPC)方式復雜,如管道、共享內存、消息隊列等。
- 線程間通信更簡單,可通過共享變量直接通信。
- 開銷
- 進程創建和切換開銷較大。
- 線程切換成本較小,效率更高。
- 崩潰影響
- 進程崩潰不會影響其他進程。
- 線程崩潰可能影響整個進程。
2. 創建線程的幾種方式?
在 Java 中,創建線程主要有以下幾種方式:
-
繼承
Thread
類class MyThread extends Thread {public void run() {System.out.println("線程執行");} }public class Main {public static void main(String[] args) {MyThread thread = new MyThread();thread.start();} }
-
實現
Runnable
接口class MyRunnable implements Runnable {public void run() {System.out.println("線程執行");} }public class Main {public static void main(String[] args) {Thread thread = new Thread(new MyRunnable());thread.start();} }
-
使用
Callable
和FutureTask
(可以返回值和拋出異常)import java.util.concurrent.*;class MyCallable implements Callable<String> {public String call() throws Exception {return "線程執行完成";} }public class Main {public static void main(String[] args) throws Exception {FutureTask<String> task = new FutureTask<>(new MyCallable());Thread thread = new Thread(task);thread.start();System.out.println(task.get());} }
-
使用線程池
ExecutorService
import java.util.concurrent.*;public class Main {public static void main(String[] args) {ExecutorService executor = Executors.newFixedThreadPool(2);executor.execute(() -> System.out.println("線程執行"));executor.shutdown();} }
3. 線程的生命周期(狀態)有哪些?
線程的生命周期可分為以下幾種狀態:
- NEW(新建狀態):線程對象被創建,但未調用
start()
。 - RUNNABLE(就緒/運行狀態):調用
start()
方法后,等待 CPU 調度。 - BLOCKED(阻塞狀態):線程試圖獲取鎖但被阻塞。
- WAITING(無限等待狀態):線程調用
wait()
或join()
,需顯式喚醒。 - TIMED_WAITING(計時等待狀態):線程調用
sleep(time)
、wait(time)
等方法。 - TERMINATED(終止狀態):線程執行完成或被異常終止。
4. 什么是守護線程(Daemon Thread)?
守護線程是后臺運行的線程,主要用于執行后臺任務,如垃圾回收。
- 特點
- 守護線程在所有非守護線程結束后,自動終止。
setDaemon(true)
方法可以設置線程為守護線程。
示例:
public class DaemonThreadExample {public static void main(String[] args) {Thread daemonThread = new Thread(() -> {while (true) {System.out.println("守護線程運行中...");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});daemonThread.setDaemon(true);daemonThread.start();try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("主線程結束");}
}
5. 線程優先級(Priority)的作用及問題?
- Java 線程優先級范圍:
1
(最低)~10
(最高)。 - 線程默認優先級是
5
。 - 使用
setPriority(int newPriority)
設置優先級。
示例:
Thread thread = new Thread(() -> System.out.println("線程運行"));
thread.setPriority(Thread.MAX_PRIORITY); // 設置最高優先級
thread.start();
問題:
- 不一定生效:線程調度由操作系統決定,Java 只是建議。
- 可能導致線程饑餓:高優先級線程可能長時間占用 CPU,低優先級線程無法執行。
- 平臺相關:不同操作系統對線程優先級的實現可能不同。
二、線程安全與鎖機制
1. 什么是線程安全?如何實現線程安全?
線程安全指的是多個線程同時訪問共享數據時,不會導致數據的不一致性或錯誤。
實現線程安全的方法:
- synchronized 關鍵字:對方法或代碼塊加鎖,確保同一時間只有一個線程訪問。
- volatile 關鍵字:保證變量的可見性,防止指令重排序。
- ReentrantLock:可重入鎖,提供更靈活的鎖控制。
- CAS(Compare-And-Swap):無鎖并發編程,保證變量的原子性更新。
- 線程安全的集合類:如
ConcurrentHashMap
、CopyOnWriteArrayList
。 - ThreadLocal:為每個線程提供獨立變量,防止共享數據沖突。
- 原子操作類:如
AtomicInteger
、AtomicReference
。
2. synchronized 關鍵字的底層實現原理(對象頭、Monitor 機制)?
synchronized
通過對象頭(Mark Word)中的鎖標志位和 Monitor 機制 實現。
- 對象頭(Mark Word):存儲鎖信息,如偏向鎖、輕量級鎖、重量級鎖。
- Monitor 機制:由操作系統的 互斥量(Mutex) 實現,線程競爭鎖時可能進入阻塞狀態。
- 鎖升級過程:
- 偏向鎖(Biased Locking)
- 輕量級鎖(Lightweight Locking)
- 重量級鎖(Heavyweight Locking)
示例:
synchronized (this) {System.out.println("同步代碼塊");
}
3. volatile 關鍵字的作用和實現原理(內存屏障、可見性、禁止指令重排)?
volatile
關鍵字作用:
- 保證可見性:修改后的值會立即刷新到主內存。
- 禁止指令重排序:防止 CPU 優化導致的順序問題。
- 不保證原子性:多個線程修改
volatile
變量仍可能導致競態條件。
底層實現:
- 通過 內存屏障(Memory Barrier) 確保可見性。
- 使用 MESI 緩存一致性協議 保證 CPU 緩存一致性。
示例:
private volatile boolean flag = true;
4. 什么是 CAS(Compare-And-Swap)?其優缺點?
CAS(比較并交換) 是一種無鎖并發機制,核心思想是:
- 讀取變量的當前值
V
。 - 如果
V
等于期望值E
,則更新為新值N
。 - 若
V
發生變化,則重新嘗試。
優點:
- 無需加鎖,性能高。
缺點:
- ABA 問題:可使用
AtomicStampedReference
解決。 - 自旋消耗 CPU:長時間自旋可能降低性能。
示例:
AtomicInteger atomicInteger = new AtomicInteger(0);
atomicInteger.compareAndSet(0, 1);
5. synchronized 和 ReentrantLock 的區別?
對比項 | synchronized | ReentrantLock |
---|---|---|
鎖的類型 | 內置鎖 | 顯式鎖 |
可重入性 | 支持 | 支持 |
公平鎖 | 不支持 | 支持 |
中斷響應 | 不支持 | 支持 |
條件變量 | 不支持 | 支持 Condition |
6. 可重入鎖(ReentrantLock)的實現原理?
可重入鎖 允許同一線程多次獲取鎖。
ReentrantLock
通過 AQS(AbstractQueuedSynchronizer) 維護一個 state 變量,記錄獲取次數。- 線程釋放鎖時,
state
遞減,直到state == 0
時真正釋放鎖。
示例:
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {System.out.println("執行任務");
} finally {lock.unlock();
}
7. 公平鎖與非公平鎖的區別?
- 公平鎖:線程按順序獲取鎖,避免線程饑餓。
- 非公平鎖:線程可以插隊獲取鎖,提高性能。
示例:
ReentrantLock lock = new ReentrantLock(true); // 公平鎖
8. 什么是死鎖?如何避免或檢測死鎖?
死鎖 是指多個線程相互等待對方釋放資源,導致程序無法繼續。
避免方法:
- 避免嵌套鎖
- 資源分配有序
- 超時機制
- 死鎖檢測工具(jstack、jconsole)
9. 偏向鎖、輕量級鎖、重量級鎖的升級過程?
鎖的升級流程:
- 無鎖狀態
- 偏向鎖:只有一個線程訪問。
- 輕量級鎖:多個線程競爭但無阻塞。
- 重量級鎖:多個線程競爭,阻塞等待。
10. 鎖消除(Lock Elimination)和鎖粗化(Lock Coarsening)的原理?
- 鎖消除:JIT 編譯時,發現局部變量不逃逸,移除鎖。
- 鎖粗化:多個連續加鎖的操作合并,減少鎖的頻繁釋放與獲取。
示例:
public void test() {StringBuilder sb = new StringBuilder(); // JIT 可能優化掉鎖sb.append("Hello");sb.append("World");
}
三、線程協作與通信
1. wait()
、notify()
、notifyAll()
的使用場景和注意事項?
-
使用場景:
wait()
、notify()
和notifyAll()
主要用于線程間的同步,適用于生產者-消費者模型或多個線程共享資源的情況。wait()
: 讓當前線程進入等待狀態,釋放鎖,讓其他線程可以獲取鎖并執行。notify()
: 喚醒一個在wait()
狀態的線程,但不會立即釋放鎖,需等待當前線程執行完畢。notifyAll()
: 喚醒所有等待的線程,但只有一個線程能獲取鎖執行,其他線程仍需等待。
-
注意事項:
wait()
、notify()
、notifyAll()
必須在同步代碼塊或同步方法中使用,否則會拋IllegalMonitorStateException
。- 調用
wait()
方法后,線程會釋放鎖,進入等待隊列,直到被notify()
或notifyAll()
喚醒。 notify()
只是通知一個等待線程,并不會立即釋放鎖,必須等待持有鎖的線程執行完畢后才能釋放。
2. sleep()
和 wait()
的區別?
對比項 | sleep() | wait() |
---|---|---|
作用 | 讓當前線程休眠一段時間 | 讓當前線程等待,直到被 notify() 喚醒 |
釋放鎖 | 不釋放鎖 | 釋放鎖 |
使用位置 | 可以在任何地方使用 | 必須在同步代碼塊或同步方法中使用 |
恢復方式 | 時間到后自動恢復運行 | 需要 notify() 或 notifyAll() 喚醒 |
拋出異常 | InterruptedException | InterruptedException |
3. 如何實現線程間通信(如生產者-消費者模型)?
示例:使用 wait()
和 notify()
實現生產者-消費者模式
class SharedResource {private int data;private boolean available = false;public synchronized void produce(int value) {while (available) {try {wait(); // 生產者等待} catch (InterruptedException e) {e.printStackTrace();}}data = value;available = true;System.out.println("生產者生產: " + value);notify(); // 喚醒消費者}public synchronized int consume() {while (!available) {try {wait(); // 消費者等待} catch (InterruptedException e) {e.printStackTrace();}}available = false;System.out.println("消費者消費: " + data);notify(); // 喚醒生產者return data;}
}
4. Condition
接口的作用及與 wait/notify
的區別?
-
作用:
Condition
提供了比wait()
和notify()
更強大的線程間通信機制。- 允許多個等待隊列,可更精細地控制線程的喚醒。
-
區別:
| 對比項 |wait()/notify()
|Condition
|
|------------|-----------------|------------|
| 依賴的鎖 | 必須配合synchronized
關鍵字使用 | 需要Lock
對象 |
| 等待方法 |wait()
|await()
|
| 通知方法 |notify()
/notifyAll()
|signal()
/signalAll()
|
| 多個等待隊列 | 只有一個等待隊列,notify()
可能會誤喚醒非目標線程 | 可以創建多個Condition
,更精準喚醒特定線程 | -
示例:使用
Condition
實現生產者-消費者模型
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;class SharedResourceWithCondition {private int data;private boolean available = false;private final Lock lock = new ReentrantLock();private final Condition condition = lock.newCondition();public void produce(int value) {lock.lock();try {while (available) {condition.await(); // 生產者等待}data = value;available = true;System.out.println("生產者生產: " + value);condition.signal(); // 喚醒消費者} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}public int consume() {lock.lock();try {while (!available) {condition.await(); // 消費者等待}available = false;System.out.println("消費者消費: " + data);condition.signal(); // 喚醒生產者return data;} catch (InterruptedException e) {e.printStackTrace();return -1;} finally {lock.unlock();}}
}
四、并發工具類(JUC)
1. CountDownLatch
、CyclicBarrier
、Semaphore
的使用場景和區別
1.1 CountDownLatch
- 作用:用于 等待多個線程執行完畢后再繼續,計數器只能減不能加。
- 使用場景:
- 任務分解:主線程等待多個子線程完成后再繼續。
- 并發測試:多個線程同時執行某個任務,主線程等待所有線程完成。
- 系統啟動依賴:等待多個資源加載完畢后再啟動服務。
import java.util.concurrent.CountDownLatch;public class CountDownLatchExample {public static void main(String[] args) throws InterruptedException {CountDownLatch latch = new CountDownLatch(3);for (int i = 0; i < 3; i++) {new Thread(() -> {System.out.println(Thread.currentThread().getName() + " 完成任務");latch.countDown();}).start();}latch.await(); // 等待所有子線程執行完畢System.out.println("所有任務完成,繼續執行主線程");}
}
1.2 CyclicBarrier
- 作用:用于 讓一組線程等待彼此到達某個同步點,并在到達后執行某個 回調任務。
- 與
CountDownLatch
的區別:CountDownLatch
只能使用一次,CyclicBarrier
可重復使用。CyclicBarrier
允許在所有線程到達屏障時執行額外任務。
- 使用場景:
- 多人游戲:等待所有玩家加載完畢后開始游戲。
- 并行計算:多線程分塊計算,待所有線程計算完成后合并結果。
import java.util.concurrent.CyclicBarrier;public class CyclicBarrierExample {public static void main(String[] args) {CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("所有線程到達屏障點"));for (int i = 0; i < 3; i++) {new Thread(() -> {System.out.println(Thread.currentThread().getName() + " 到達屏障");try {barrier.await();} catch (Exception e) {e.printStackTrace();}}).start();}}
}
1.3 Semaphore
- 作用:限流,用于控制并發線程數,類似停車場的車位管理。
- 使用場景:
- 限制數據庫連接數。
- 控制接口并發訪問,防止系統崩潰。
- 線程池限流,限制任務提交速率。
import java.util.concurrent.Semaphore;public class SemaphoreExample {public static void main(String[] args) {Semaphore semaphore = new Semaphore(2);for (int i = 0; i < 5; i++) {new Thread(() -> {try {semaphore.acquire();System.out.println(Thread.currentThread().getName() + " 獲取資源");Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + " 釋放資源");semaphore.release();} catch (InterruptedException e) {e.printStackTrace();}}).start();}}
}
2. Exchanger
的作用
-
作用:兩個線程間的數據交換工具,用于在兩個線程之間安全地交換對象。
-
特點:
- 線程 A 調用
exchange()
方法后會阻塞,直到線程 B 也調用exchange()
交換數據。 - 適用于 生產者-消費者模型,當生產者產生數據,消費者等待數據時,
Exchanger
可用作數據緩沖區。
- 線程 A 調用
-
使用場景:
- 數據處理:一個線程生產數據,另一個線程處理數據。
- 大數據計算:并行計算任務的階段性交換。
3. Phaser
的使用場景
-
作用:可變并發任務同步工具,類似
CyclicBarrier
,但支持 動態注冊/注銷線程。 -
區別:
CyclicBarrier
需要指定固定數量的線程,Phaser
支持動態增減線程。Phaser
適用于多階段任務,而CyclicBarrier
適用于單次屏障。
-
使用場景:
- 分階段任務:如多輪競賽,每輪線程數不同。
- 遞歸任務:動態管理并發線程的執行。
4. Future
和 CompletableFuture
的區別
對比項 | Future | CompletableFuture |
---|---|---|
異步能力 | 只能獲取異步任務結果,不支持回調 | 支持回調和流式操作,能更好地管理異步流程 |
阻塞獲取結果 | 需要 get() 方法,可能會阻塞線程 | 支持 thenApply() 等非阻塞操作 |
支持組合任務 | 不支持多個 Future 組合 | thenCombine() 支持多個任務組合 |
異常處理 | 只能手動 try-catch 處理 | exceptionally() 處理異常更方便 |
取消任務 | cancel() 方法 | cancel() 也可用 |
CompletableFuture
適用于更復雜的異步任務鏈,能提高代碼可讀性和效率。
5. StampedLock
的優化點及使用場景
優化點
-
支持三種模式:
- 寫鎖 (
writeLock()
):獨占鎖,類似ReentrantReadWriteLock
的寫鎖。 - 讀鎖 (
readLock()
):共享鎖,多個線程可同時讀。 - 樂觀讀 (
tryOptimisticRead()
):不阻塞寫入,提高性能,適用于讀多寫少場景。
- 寫鎖 (
-
性能優勢:
- 避免讀鎖競爭:樂觀讀鎖不會阻塞寫操作,適用于高讀低寫的情況。
- 降低鎖的升級開銷:先嘗試樂觀讀,再回退為悲觀鎖,減少鎖沖突。
使用場景
- 緩存系統:大多數操作是讀取,寫入較少的情況,避免
ReentrantReadWriteLock
造成的讀鎖開銷。 - 多線程計數器:多個線程同時讀計數值,少數線程寫入。
- 金融交易系統:讀取賬戶余額時使用樂觀鎖,只有在余額變更時才獲取寫鎖。
import java.util.concurrent.locks.StampedLock;public class StampedLockExample {private int count = 0;private final StampedLock lock = new StampedLock();public int optimisticRead() {long stamp = lock.tryOptimisticRead();int current = count;if (!lock.validate(stamp)) {stamp = lock.readLock();try {current = count;} finally {lock.unlockRead(stamp);}}return current;}public void write(int value) {long stamp = lock.writeLock();try {count = value;} finally {lock.unlockWrite(stamp);}}
}
五、 線程池相關問題解析
1. 線程池的核心參數及作用
-
核心線程數 (corePoolSize)
指定了線程池中始終保持運行的最小線程數。當有新任務到達時,線程池會優先創建新線程直到達到核心線程數,之后任務才會進入隊列等待執行。 -
最大線程數 (maximumPoolSize)
表示線程池允許創建的最大線程數。當隊列已滿且核心線程都在忙碌時,線程池會創建新線程直至達到最大線程數。 -
任務隊列 (workQueue)
用于存放等待執行的任務。常用隊列類型有有界隊列(如ArrayBlockingQueue
)和無界隊列(如LinkedBlockingQueue
)。隊列的選擇會影響任務調度的策略和系統資源使用。 -
線程空閑時間 (keepAliveTime)
當線程數超過核心線程數時,多余線程在空閑狀態下等待新任務的最長時間,超時后這些線程會被終止,以節省資源。 -
線程工廠 (threadFactory)
用于創建新線程,可以通過自定義線程工廠設置線程名稱、優先級、守護狀態等,從而便于線程管理和調試。 -
拒絕策略 (rejectedExecutionHandler)
當線程池飽和(即線程數已達最大值且任務隊列也滿)時,對新提交任務的處理策略,如拋異常、調用者運行、丟棄任務或丟棄隊列中最老任務等。
2. 線程池的工作流程(任務提交后的處理邏輯)
-
任務提交
當調用execute()
或submit()
方法提交任務時,線程池首先判斷當前線程數是否小于核心線程數。 -
創建核心線程
如果當前線程數小于核心線程數,則立即創建新線程執行任務,而不是將任務放入隊列。 -
任務入隊
當核心線程數已滿,新的任務將被放入任務隊列中等待執行。 -
創建非核心線程
如果任務隊列已滿,并且當前線程數小于最大線程數,則線程池會創建新線程來處理額外的任務。 -
拒絕策略觸發
當任務隊列已滿且線程數已經達到最大線程數時,線程池根據配置的拒絕策略處理新提交的任務。 -
線程復用與銷毀
完成任務后,線程不會立即銷毀,而是進入等待狀態以便復用。當線程超過核心線程數且長時間空閑,則會被回收。
3. 常見的線程池類型及其問題
-
FixedThreadPool
- 特點:固定線程數,不會動態增減。
- 問題:若任務執行緩慢或任務積壓,可能導致隊列中任務長時間等待,無法利用資源動態擴展。
-
CachedThreadPool
- 特點:根據任務需要動態創建線程,線程空閑時會被回收,適用于任務執行時間較短的場景。
- 問題:在任務激增時可能會創建大量線程,導致系統資源耗盡,甚至引發性能問題。
-
SingleThreadExecutor
- 特點:單線程順序執行所有任務,適用于需要保證任務順序的場景。
- 問題:單線程可能成為性能瓶頸,且任務過多時容易造成任務堆積。
4. 線程池的拒絕策略
-
AbortPolicy (默認策略)
當任務無法執行時,直接拋出RejectedExecutionException
異常,中斷任務提交流程。 -
CallerRunsPolicy
由任務提交者所在的線程執行該任務,從而降低新任務的提交速率,緩解線程池壓力。 -
DiscardPolicy
直接丟棄新提交的任務,不會拋出異常,但可能導致部分任務未被執行。 -
DiscardOldestPolicy
丟棄任務隊列中等待最久的任務,然后嘗試提交當前任務,這樣可以為新的任務騰出空間。
5. 如何合理配置線程池參數
-
根據任務性質配置線程數
- CPU密集型任務:通常將線程數設置為 CPU 核心數或略微超出。
- IO密集型任務:線程數可以設置為 CPU 核心數的多倍,因為等待時間較長。
-
任務隊列的選擇
根據任務特性選擇合適的隊列類型和容量。無界隊列雖能避免拒絕任務,但可能導致任務堆積;有界隊列則能限制任務數量,但在高負載時容易觸發拒絕策略。 -
合理設置
keepAliveTime
防止非核心線程在任務較少時占用系統資源,確保線程在空閑超時后被回收。 -
拒絕策略的選取
根據系統對任務丟失或延遲的容忍度選擇合適的拒絕策略,保障系統穩定性。 -
動態調整策略
根據實際運行情況,監控任務執行情況和系統資源使用情況,動態調整線程池參數以達到最佳性能。
6. 線程池中線程復用(Thread Reuse)的原理
-
預先創建與等待
線程池在初始化或任務提交時創建一定數量的核心線程,這些線程在完成任務后不會被銷毀,而是進入等待狀態,以便立即處理后續任務。 -
任務隊列
線程通過不斷從任務隊列中取任務來執行,實現線程的重復利用,避免頻繁創建和銷毀線程帶來的開銷。 -
KeepAlive 機制
對于非核心線程,在任務執行完畢后,如果在指定的空閑時間內沒有新任務到達,則會被回收。這樣既能保證高負載時的擴展能力,也能在低負載時節省資源。
示例代碼
下面是一個簡單的 Java 示例,展示如何使用線程池提交任務。注意,Java 代碼已做適當轉義:
// 示例:使用線程池提交任務的簡單代碼
import java.util.concurrent.*;public class ThreadPoolExample {public static void main(String[] args) {// 創建一個固定大小的線程池,核心線程數和最大線程數都為4ExecutorService executor = Executors.newFixedThreadPool(4);for (int i = 0; i < 10; i++) {executor.submit(new Runnable() {@Overridepublic void run() {System.out.println("執行任務 - " + Thread.currentThread().getName());}});}// 關閉線程池,等待所有任務執行完畢后退出executor.shutdown();}
}
六、 高級并發特性解析
1. Java 內存模型(JMM)的核心概念
-
主內存與工作內存
每個線程都有自己的工作內存,用于保存共享變量的副本,而共享變量本身存儲在主內存中。線程對變量的所有操作都必須先在工作內存中進行,再同步到主內存,從而保證了數據的一致性和線程間的可見性。 -
happens-before 原則
規定了操作之間的先后關系,確保一個操作的結果在另一個操作執行前是可見的。典型規則包括:- 程序順序規則:單線程中代碼按照順序執行。
- 鎖規則:解鎖操作 happens-before 后續對同一鎖的加鎖操作。
- volatile 規則:volatile 寫操作 happens-before 隨后的 volatile 讀操作。
- 線程啟動規則:線程啟動前的操作 happens-before 線程內的所有操作。
2. 原子性、可見性、有序性及其保證方法
-
原子性
一個操作是不可分割的,要么全部執行,要么全部不執行。
保證方法:- 使用
synchronized
關鍵字或顯示鎖(如ReentrantLock
)來保證復合操作的原子性。 - 利用 Java 的原子類(如
AtomicInteger
)實現無鎖的原子操作。
- 使用
-
可見性
確保當一個線程修改了共享變量,其他線程能夠及時看到這個修改。
保證方法:- 使用
volatile
關鍵字,確保變量的修改立即刷新到主內存。 - 使用鎖(
synchronized
或Lock
)來保證操作的可見性。
- 使用
-
有序性
保證程序中指令按照代碼順序執行(在單線程環境下成立)。
保證方法:- 使用
synchronized
或volatile
提供的內存屏障,防止指令重排序。 - JMM 中的 happens-before 規則確保了操作的有序性。
- 使用
3. 指令重排序及其避免方法
-
指令重排序原理
為了優化性能,編譯器和處理器可能會重新排序指令。雖然在單線程環境中不會改變程序的正確性,但在多線程環境中可能會導致數據不一致的問題。 -
避免方法:
- 內存屏障
利用內存屏障指令保證某些操作在執行順序上的嚴格順序,從而防止重排序問題。 - volatile 關鍵字
使用 volatile 修飾的變量能確保寫操作不會被重排序到讀操作之后,并保證了可見性。
- 內存屏障
4. ThreadLocal 的原理、使用場景及內存泄漏問題
-
原理
每個線程內部都有一個 ThreadLocalMap,其中存儲了 ThreadLocal 對象和對應的線程局部變量。這樣,每個線程都可以獨立訪問和修改自己的變量副本,而不會影響其他線程。 -
使用場景
- 需要線程隔離的變量,如用戶會話、數據庫連接等。
- 避免在多線程環境中共享數據而導致的同步問題。
-
內存泄漏問題
當線程使用完 ThreadLocal 后,如果沒有調用remove()
方法清理 ThreadLocalMap,可能會導致內存泄漏,特別是在使用線程池等長生命周期線程時。
解決方法:- 在任務執行結束后主動調用
ThreadLocal.remove()
清理數據。 - 注意設計長生命周期線程中 ThreadLocal 的使用,避免不必要的數據駐留。
- 在任務執行結束后主動調用
5. Fork/Join 框架與工作竊取機制
-
Fork/Join 框架
是 Java 7 引入的一個并行計算框架,適用于將大任務拆分成多個小任務(Fork),并行執行后將結果合并(Join)。核心組件包括ForkJoinPool
、ForkJoinTask
以及其子類RecursiveTask
(有返回值)和RecursiveAction
(無返回值)。 -
工作竊取機制 (Work-Stealing)
每個工作線程維護一個雙端隊列,當線程完成了自己隊列中的任務時,會嘗試從其他線程隊列的尾部“竊取”任務以保持高效的資源利用和負載均衡。
示例代碼
以下是一個使用 Fork/Join 框架計算數組求和的示例代碼,注意 Java 代碼中的特殊字符均已轉義:
// 示例:使用 Fork/Join 框架計算數組求和
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;public class ForkJoinSum extends RecursiveTask<Long> {private static final int THRESHOLD = 1000;private long[] arr;private int start;private int end;public ForkJoinSum(long[] arr, int start, int end) {this.arr = arr;this.start = start;this.end = end;}@Overrideprotected Long compute() {if (end - start <= THRESHOLD) {long sum = 0;for (int i = start; i < end; i++) {sum += arr[i];}return sum;} else {int mid = (start + end) / 2;ForkJoinSum leftTask = new ForkJoinSum(arr, start, mid);ForkJoinSum rightTask = new ForkJoinSum(arr, mid, end);leftTask.fork(); // 異步執行左邊任務long rightResult = rightTask.compute(); // 同步執行右邊任務long leftResult = leftTask.join(); // 等待左邊任務完成return leftResult + rightResult;}}public static void main(String[] args) {long[] array = new long[10000];// 初始化數組for (int i = 0; i < array.length; i++) {array[i] = i;}ForkJoinPool pool = new ForkJoinPool();ForkJoinSum task = new ForkJoinSum(array, 0, array.length);long result = pool.invoke(task);System.out.println("Sum: " + result);}
}
該示例展示了如何將一個大任務(數組求和)拆分成多個小任務,通過 Fork/Join 框架的工作竊取機制并行處理任務,并最終合并結果。
七、 并發容器解析
1. ConcurrentHashMap 的實現原理(JDK7 vs JDK8)
-
JDK7 實現原理
- 分段鎖(Segment)機制:將整個 Map 劃分為多個 Segment,每個 Segment 內部維護一部分哈希桶。
- 鎖定粒度較小:操作某個鍵值對時,僅鎖定所在的 Segment,讀操作大多無鎖,從而提升并發度。
- 局限性:Segment 數量在創建時固定,可能存在熱點 Segment 導致爭用。
-
JDK8 實現原理
- 取消分段鎖:直接使用一個 Node 數組作為底層結構,通過 CAS 操作實現無鎖更新。
- 細粒度鎖:在發生沖突時,僅對鏈表或紅黑樹的某個桶加鎖,而非整個 Segment。
- 鏈表轉紅黑樹:當單個桶中鏈表長度超過閾值時,轉換為紅黑樹以降低查找時間。
- 優勢:讀操作完全無鎖,寫操作在沖突較少時可通過 CAS 完成,提高了整體性能和擴展性。
2. CopyOnWriteArrayList 的適用場景及優缺點
-
適用場景
- 讀操作遠遠多于寫操作的場景,如緩存、事件監聽器列表等。
- 需要保證在迭代時數據不被修改,避免
ConcurrentModificationException
的情況。
-
優點
- 無鎖讀操作:迭代時不需要加鎖,性能高且線程安全。
- 簡單安全:每次寫操作都會復制整個底層數組,保證了數據的一致性。
-
缺點
- 寫操作開銷大:每次更新都會復制整個數組,對于寫密集型應用效率低下。
- 內存占用增加:復制操作會導致額外的內存消耗,適用于數據量較小且變更較少的場景。
3. BlockingQueue 的實現類及使用場景
-
ArrayBlockingQueue
- 實現原理:基于數組實現的有界阻塞隊列,內部通過
ReentrantLock
來保證線程安全。 - 使用場景:適用于固定容量的緩沖區,如生產者消費者模式中限制任務數量,防止過載。
- 實現原理:基于數組實現的有界阻塞隊列,內部通過
-
LinkedBlockingQueue
- 實現原理:基于鏈表實現,可設置有界或無界隊列,采用分離的
putLock
和takeLock
提高并發性能。 - 使用場景:適用于需要較大容量緩沖,或不希望受到固定數組大小限制的場景。
- 實現原理:基于鏈表實現,可設置有界或無界隊列,采用分離的
-
其他實現類
- PriorityBlockingQueue:基于優先級的無界阻塞隊列,適用于需要按照優先級處理任務的場景。
- DelayQueue:基于延時策略的隊列,用于任務調度。
- SynchronousQueue:每個插入操作必須等待對應的移除操作,適合于任務直接交給消費者處理的場景。
4. ConcurrentLinkedQueue 的無鎖實現原理
- 基于鏈表的無鎖隊列:采用鏈表結構作為底層數據結構。
- CAS 操作:利用 CAS(Compare-And-Swap)原子操作來保證在高并發環境下的線程安全,無需顯式鎖。
- 頭尾指針維護:通過原子變量維護頭結點和尾結點,入隊和出隊操作通過 CAS 更新指針,保證數據一致性。
- 優點:高并發情況下性能優越,避免了鎖競爭;
- 缺點:在極端情況下可能存在短暫的不一致狀態,但總體上能夠保證最終一致性。
八、 并發設計模式與實戰
1. 生產者-消費者模式的實現方式
- 阻塞隊列實現
利用阻塞隊列(如ArrayBlockingQueue
、LinkedBlockingQueue
)實現任務緩沖區,生產者調用put()
將任務放入隊列,消費者調用take()
從隊列中獲取任務。 - 等待/通知機制
在共享緩沖區上使用wait()
與notify()/notifyAll()
實現線程間通信,但需要小心處理虛假喚醒和同步問題。 - 信號量機制
通過Semaphore
控制緩沖區中可用資源數量,協調生產者和消費者的訪問。
2. 如何實現線程安全的單例模式
- 雙重檢查鎖定(DCL)
在實例為空時進入同步代碼塊,再次檢查后創建實例。需要將實例聲明為volatile
,防止指令重排序導致問題。 - 靜態內部類
利用 JVM 類加載機制,在第一次使用時創建單例,既保證線程安全,又實現延遲加載。 - 枚舉實現
使用枚舉類型,JVM 會保證枚舉實例的唯一性和線程安全。
示例:使用雙重檢查鎖定實現線程安全的單例模式
(注意:Java代碼中的特殊字符已轉義)
public class Singleton {private static volatile Singleton instance;private Singleton() {// 防止反射調用if (instance != null) {throw new IllegalStateException("Already initialized");}}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}
3. 如何排查和解決死鎖問題
排查方法
- 使用線程轉儲(Thread Dump)工具分析線程的鎖持有情況;
- 利用 JDK 內置的監控工具(如 VisualVM、JConsole、ThreadMXBean)檢測死鎖;
- 檢查代碼中鎖的獲取順序是否一致,是否存在資源循環等待。
解決措施
- 統一鎖順序:確保所有線程按照相同順序獲取多個鎖;
- 使用超時鎖:采用
tryLock()
設置超時,避免長時間等待; - 細化鎖粒度:減少單個鎖保護的資源范圍,或采用無鎖算法與樂觀鎖。
4. 如何避免競態條件(Race Condition)
加鎖同步
- 使用
synchronized
或ReentrantLock
等互斥機制,確保同一時間只有一個線程訪問共享數據。
原子操作
- 利用原子類(如
AtomicInteger
、AtomicLong
)保證對單個變量的原子更新。
不可變對象設計
- 使用不可變對象,避免在并發場景下對對象狀態的修改。
線程安全的集合
- 采用并發容器(如
ConcurrentHashMap
、CopyOnWriteArrayList
)管理共享數據,避免并發修改問題。
5. 如何設計高并發場景下的計數器
原子變量
- 使用
AtomicInteger
或AtomicLong
提供無鎖計數操作,適用于簡單的計數場景。
LongAdder/LongAccumulator
- 在高并發場景下,使用
LongAdder
(JDK8及以上)分散熱點,多個計數單元并行累加,最后合并結果。
分段計數器
- 將計數器分為多個獨立部分,每個線程或線程組維護各自的局部計數,定期合并成全局計數,降低鎖競爭。
九、底層原理與擴展
1. Java 線程與操作系統線程的關系
- 映射關系:Java 線程是由 JVM 映射到底層操作系統線程,通常采用 1:1 模型,即每個 Java 線程對應一個操作系統線程。
- 操作系統調度:操作系統負責線程的調度、上下文切換等底層管理,JVM 則在此基礎上提供高級的線程管理功能,如線程池和并發工具。
- 資源管理:底層線程由操作系統管理資源,Java 線程的創建、銷毀和調度都依賴于操作系統的支持。
2. 什么是協程(Coroutine)?Java 中的虛擬線程(Loom 項目)
-
協程(Coroutine)
- 協程是一種輕量級的線程實現,可以在單個線程中實現多個任務之間的切換。
- 它通過保存和恢復執行上下文來實現并發執行,切換開銷遠低于操作系統線程的上下文切換。
- 協程通常由用戶級庫調度,不依賴于操作系統的線程管理。
-
Java 中的虛擬線程(Loom 項目)
- Project Loom 是 Java 的一個實驗性項目,旨在引入虛擬線程這一概念,使得創建成千上萬的線程成為可能。
- 虛擬線程基于協程思想,能夠在用戶空間內高效調度,極大降低線程切換和資源消耗。
- 該項目有望簡化并發編程模型,使得編寫高并發應用程序更加直觀和高效。
3. 如何通過 jstack 分析線程狀態
- 生成線程堆棧信息:使用命令
jstack <pid>
(其中<pid>
是 Java 進程的標識)獲取當前線程的狀態及堆棧信息。 - 分析線程狀態:
- 查看各線程的狀態,如
RUNNABLE
、WAITING
、BLOCKED
等。 - 檢查線程等待鎖和持有鎖的信息,確定是否存在死鎖或鎖競爭問題。
- 根據堆棧信息定位長時間阻塞或運行緩慢的線程,幫助分析性能瓶頸或故障原因。
- 查看各線程的狀態,如
4. 什么是偽共享(False Sharing)?如何避免?
-
偽共享定義:
- 當多個線程在不同的變量上操作時,如果這些變量恰好位于同一個 CPU 緩存行中,會引起頻繁的緩存行失效和重載,進而影響性能。
-
避免方法:
- 內存填充(Padding):通過在變量之間添加無用的數據字段(例如額外的 long 型字段)來確保它們不在同一緩存行上。
- 使用 @Contended 注解:在 Java 8 及以上版本中,可以使用
@Contended
注解標記容易發生偽共享的變量(需在 JVM 啟動參數中啟用-XX:-RestrictContended
)。 - 合理的數據結構設計:在設計并發數據結構時,考慮將易沖突的變量分散到不同的緩存行中,減少互相影響。
十、其他擴展問題
1. 如何實現異步編程(如 CompletableFuture、Reactive Streams)?
CompletableFuture
CompletableFuture
是 Java 8 引入的異步編程工具,它支持鏈式操作,可以避免傳統回調地獄(Callback Hell)。- 主要方法包括
supplyAsync()
、thenApply()
、thenAccept()
、thenCompose()
、handle()
等。
import java.util.concurrent.CompletableFuture;public class AsyncExample {public static void main(String[] args) {CompletableFuture.supplyAsync(() -> {return "Hello, World!";}).thenApply(result -> {return result + " - Processed";}).thenAccept(System.out::println);}
}
Reactive Streams
Reactive Streams
是一種基于發布-訂閱模式的異步處理標準,支持 非阻塞 與 背壓(Back Pressure) 機制。- 常見實現有 Project Reactor(Spring WebFlux)和 RxJava。
- 核心組件:
- Publisher(發布者):生產數據流。
- Subscriber(訂閱者):消費數據流。
- Subscription(訂閱):連接
Publisher
和Subscriber
,管理數據傳輸速率。 - Processor(處理器):既是
Publisher
也是Subscriber
,可對流數據進行處理。
2. 分布式鎖與單機鎖的區別
單機鎖
- 僅用于單個 JVM 內部,保證線程間同步。
- 實現方式:
synchronized
關鍵字ReentrantLock
可重入鎖
- 優點:實現簡單、開銷低。
- 缺點:無法跨進程或跨服務器同步。
分布式鎖
- 用于多個進程或服務器之間的同步控制。
- 常見實現:
- 基于 Redis:如
SET NX EX
方式。 - 基于 ZooKeeper:利用
EPHEMERAL
(臨時節點)實現鎖。 - 基于數據庫:使用數據庫表中的唯一字段實現鎖(效率較低)。
- 基于 Redis:如
- 優點:支持跨服務同步,防止分布式環境下的數據競爭。
- 缺點:實現復雜,需要考慮網絡延遲、鎖超時、鎖丟失等問題。
import redis.clients.jedis.Jedis;public class RedisLock {private Jedis jedis;private String lockKey = "distributed_lock";public RedisLock(Jedis jedis) {this.jedis = jedis;}public boolean lock() {return "OK".equals(jedis.set(lockKey, "locked", "NX", "EX", 10));}public void unlock() {jedis.del(lockKey);}
}
3. 無鎖編程(Lock-Free)的實現思路
CAS(Compare-And-Swap)
- CAS 操作:比較并交換,保證多個線程同時更新時的原子性。
- Java 中的無鎖實現:
AtomicInteger
AtomicLong
AtomicReference
- 缺點:
- 可能出現 ABA 問題(即值改變了但看起來未變)。
- 高并發下可能導致 自旋過多,消耗 CPU。
import java.util.concurrent.atomic.AtomicInteger;public class CASExample {private static AtomicInteger count = new AtomicInteger(0);public static void main(String[] args) {count.incrementAndGet(); // 使用 CAS 增加計數System.out.println("Counter: " + count.get());}
}
樂觀鎖
- 原理:不主動加鎖,而是在更新前檢查數據版本,若檢測到沖突則重試。
- 實現方式:
- CAS(Compare-And-Swap) 操作
- 版本號機制(如數據庫的
version
字段)
- 適用于:高并發、沖突較少的場景。
無鎖數據結構
- 設計時避免使用鎖,如 ConcurrentLinkedQueue 采用 CAS + 鏈表的方式更新數據。
- 示例:
ConcurrentLinkedQueue
LongAdder
(分段計數,減少鎖競爭)CopyOnWriteArrayList
(寫時復制)
4. 高并發場景下常見的性能優化手段
1. 減少鎖競爭
- 使用 細粒度鎖 或 分段鎖 降低鎖競爭。
- 無鎖數據結構 代替同步容器(如
ConcurrentHashMap
)。 - 讀寫鎖 代替互斥鎖(
ReentrantReadWriteLock
)。
2. 線程池優化
- 合理配置線程池:
- CPU 密集型任務:線程數 = CPU 核心數 + 1
- IO 密集型任務:線程數 = CPU 核心數 * 2
- 避免頻繁創建/銷毀線程,實現線程復用。
3. 緩存機制
- 本地緩存(如
Caffeine
、Guava Cache
)。 - 分布式緩存(如
Redis
、Memcached
)。 - 讀寫分離、數據預熱,減少數據庫壓力。
4. 異步 & 批量處理
- 采用消息隊列(MQ),如
Kafka
、RabbitMQ
進行異步處理。 - 批量操作(如數據庫批量插入
batch insert
)。
5. 數據結構優化
- 選擇合適的數據結構,避免不必要的鎖競爭:
ConcurrentHashMap
代替Hashtable
CopyOnWriteArrayList
代替ArrayList
(適用于讀多寫少的場景)
- 避免偽共享(False Sharing),通過 緩存行填充 提高 CPU 緩存命中率。