1. 什么是線程池?它的核心原理是什么?
什么是線程池?
線程池是一種基于池化思想管理和使用線程的機制。它內部維護了多個線程,等待著分配由用戶提交的并發執行的任務。這避免了頻繁創建和銷毀線程帶來的開銷,從而提高了系統的響應速度和資源利用率。
核心原理:
線程池的核心原理是?“線程復用”?和?“資源控制”。
- 線程復用: 傳統的“一任務一線程”模式在任務執行完畢后,線程就會銷毀。線程池則讓核心線程在執行完任務后不會立即銷毀,而是處于等待狀態,去獲取新的任務來執行。這樣就省去了頻繁創建和銷毀線程的巨大開銷(包括系統調用、內存分配、資源初始化等)。
- 資源控制: 線程池允許我們設置資源池的大小(核心線程數、最大線程數),從而控制并發線程的數量,防止無限制地創建線程導致系統資源被耗盡、CPU過度切換,從而保證系統的穩定性和性能。
線程池的工作流程通常通過其內部的任務隊列和一套明確的規則來管理,其核心執行邏輯可以用下圖清晰地展示:
2. 線程池大小設置為多少更加合適?
這是一個沒有固定答案的問題,需要根據具體的應用場景和硬件資源進行權衡和測試。但有一些通用的指導原則和計算公式:
核心考量因素:
- 任務類型:任務是?CPU密集型?還是?IO密集型?
- CPU密集型:任務主要消耗CPU資源,大部分時間都在進行計算。例如:復雜的數學運算、圖像處理、視頻編碼等。
- IO密集型:任務大部分時間在等待IO操作(如磁盤讀寫、網絡請求、數據庫查詢等),CPU空閑時間較多。
經驗公式:
- 對于CPU密集型應用:線程數應接近CPU核心數,以避免過多的線程上下文切換開銷。
N_threads = N_cpu + 1
?(一個額外的線程用于在發生頁錯誤等暫停時,確保CPU時鐘周期不會被浪費) - 對于IO密集型應用:線程數可以設置得更多一些,因為CPU有很多空閑時間可以去執行其他線程的任務。
N_threads = N_cpu * U_cpu * (1 + W/C)
N_cpu
:CPU核心數(可通過?Runtime.getRuntime().availableProcessors()
?獲取)U_cpu
:期望的CPU利用率(0 <= U_cpu <= 1)W/C
:等待時間(Wait)與計算時間(Compute)的比值
實際應用:
在實際開發中,通常先使用上述公式得到一個理論值,然后通過壓力測試來不斷調整和驗證,找到最適合當前系統的線程池大小。例如,一個常見的IO密集型應用(如Web服務器)可能會將線程池大小設置為?2 * N_cpu
?到?幾倍甚至幾十倍N_cpu
?之間。
3. 線程池有哪幾種類型?各有什么優缺點?
Java通過?Executors
?工廠類提供了幾種常見的線程池:
線程池類型 | 創建方法 | 工作原理 | 優點 | 缺點 | 適用場景 |
---|---|---|---|---|---|
FixedThreadPool (固定大小線程池) | Executors.newFixedThreadPool(int nThreads) | 核心線程數 = 最大線程數,使用無界的?LinkedBlockingQueue 。 | 可以控制最大并發數,提高系統資源利用率。 | 無界隊列,可能堆積大量請求,導致OOM。 | 適用于處理CPU密集型任務,需要限制線程數量的場景。 |
CachedThreadPool (可緩存線程池) | Executors.newCachedThreadPool() | 核心線程數為0,最大線程數為Integer.MAX_VALUE ,使用同步隊列?SynchronousQueue 。空閑線程存活60秒。 | 彈性高,應對大量短期異步任務時性能好。 | 幾乎不限制線程數,可能創建過多線程,導致OOM。 | 適用于執行很多短期異步任務,或負載較輕的服務器。 |
SingleThreadExecutor (單線程池) | Executors.newSingleThreadExecutor() | 核心線程數=最大線程數=1,使用無界的?LinkedBlockingQueue 。 | 保證所有任務按提交順序串行執行。 | 無界隊列,可能堆積大量請求,導致OOM。 | 適用于需要順序執行任務的場景,如日志記錄。 |
ScheduledThreadPool (定時任務線程池) | Executors.newScheduledThreadPool(int coreSize) | 核心線程數由參數指定,最大線程數為Integer.MAX_VALUE ,使用特殊的?DelayedWorkQueue 。 | 可以定時或周期性執行任務。 | 同樣存在創建過多線程的風險。 | 執行定時任務、周期性任務,如心跳檢測、數據同步等。 |
重要提示:
FixedThreadPool
?和?SingleThreadExecutor
?因為使用無界隊列,CachedThreadPool
?和?ScheduledThreadPool
?因為最大線程數近乎無限,在任務提交速度遠大于處理速度時,都有可能導致內存溢出(OOM)。因此,阿里巴巴Java開發手冊強制要求使用?ThreadPoolExecutor
?的構造函數來手動創建線程池,以便對線程池參數有更清晰的認識和控制。
4. 什么是ThreadLocal?它的實現原理是什么?
什么是ThreadLocal?
ThreadLocal
?提供了線程局部變量。這些變量與普通變量不同,每個訪問該變量的線程都有自己獨立初始化的變量副本,實現了線程間的數據隔離。
實現原理:
ThreadLocal
?的核心原理在于每個?Thread
?對象內部都維護了一個?ThreadLocalMap
?類型的變量?threadLocals
。
ThreadLocalMap
?是一個定制化的哈希表,其?Key
?是?ThreadLocal
?對象本身(使用弱引用),Value
?是我們設置的變量副本。- 當我們調用?
threadLocal.set(value)
?時,實際上是以當前?ThreadLocal
?實例為 Key,將要存儲的值作為 Value,存入當前線程的?threadLocals
?這個 Map 中。 - 當我們調用?
threadLocal.get()
?時,它首先獲取當前線程,然后拿到當前線程的?threadLocals
?Map,再以當前?ThreadLocal
?實例為 Key 去查找對應的 Value。
簡單來說,數據并不保存在?ThreadLocal
?本身,而是保存在線程的?threadLocals
?屬性中,由?ThreadLocal
?對象作為訪問的鑰匙。
5. ThreadLocal為什么會導致內存泄漏?如何解決的?ThreadLocal的應用場景有哪些?
為什么會導致內存泄漏?
內存泄漏的根本原因是?ThreadLocalMap
?中?Entry
?的 Key 對?ThreadLocal
?實例是弱引用(WeakReference),而 Value 是強引用。
- 弱引用的Key: 當外界對?
ThreadLocal
?實例的強引用消失后(例如?threadLocal = null
),由于?Entry
?的 Key 是弱引用,在下次GC時,這個 Key 會被回收,導致?Entry
?的?Key = null
。 - 強引用的Value: 但是,
Entry
?中的 Value 仍然被一個強引用關聯著(通過?Thread -> ThreadLocalMap -> Entry -> Value
?這條鏈)。只要線程不死(例如線程池中的核心線程會常駐),這個 Value 對象就永遠不會被回收,從而造成內存泄漏。
如何解決?
- 良好編程習慣: 在使用完?
ThreadLocal
?后,必須手動調用其?remove()
?方法,將當前線程的?ThreadLocalMap
?中對應的 Entry 徹底刪除,斷開對 Value 的強引用。 - JDK的設計:?
ThreadLocal
?本身也做了一些努力,在?set()
,?get()
,?remove()
?方法中,會嘗試清理 Key 為 null 的 Entry。但這是一種被動清理,不能完全依賴。
應用場景:
- 數據庫連接(Connection)和事務(Transaction)管理: 將一個連接綁定到當前線程,保證一個事務中的所有操作使用的是同一個連接。
- Session管理: 在Web開發中,將用戶會話信息存儲到?
ThreadLocal
?中,便于在同一次請求的各個層級中獲取。 - 全局參數傳遞: 避免在方法調用鏈中層層傳遞上下文參數(如用戶身份信息、語言環境等),直接從?
ThreadLocal
?中獲取。 - 日期格式化:?
SimpleDateFormat
?是非線程安全的,可以為每個線程創建一個獨立的副本。
6. CyclicBarrier和CountDownLatch有什么區別?
特性 | CountDownLatch | CyclicBarrier |
---|---|---|
核心機制 | 一個或多個線程等待一組操作完成 | 一組線程相互等待,直到所有線程都到達一個公共屏障點 |
計數器 | 遞減計數,不可重置 | 遞增計數,可重置 (reset() ) |
可重復使用 | 否,計數器為0后不能再使用 | 是,通過重置計數器可以循環使用 |
主要方法 | await() ,?countDown() | await() |
等待者 | 一個或多個等待線程 | 所有互相等待的線程本身都是屏障點的一部分 |
常見應用場景 | 主線程等待多個子線程完成任務后再繼續 | 多線程計算數據,最后合并計算結果;多人游戲等待所有玩家準備完畢 |
簡單比喻:
- CountDownLatch: 就像倒計時發車。司機(主線程)要等所有乘客(多個操作)都上車(
countDown()
)后,才能發動汽車。 - CyclicBarrier: 就像團隊旅行。必須所有成員(所有線程)都到達集合點(
await()
)后,才能一起出發去下一個景點。
7. CopyOnWriteArrayList底層原理是什么?
原理:寫時復制(Copy-On-Write)
- 讀取: 所有讀取操作(
get
,?iterator
)都是直接在一個不變的數組快照上進行的,不需要加鎖,性能極高且安全。 - 寫入/修改: 當執行寫入操作(
add
,?set
,?remove
)時,它會將底層原有的數組完整地復制(Copy)一份到一個新數組中,然后在這個新數組上進行修改操作。 - 更新引用: 修改完成后,將底層數組的引用指向這個新數組,替換掉舊的數組。
- 丟棄舊數據: 舊的數組如果沒有被引用,會被GC回收。
優缺點:
- 優點: 讀寫分離,讀操作完全無鎖,性能非常高,非常適合讀多寫少的場景。
- 缺點:
- 內存占用大: 每次寫操作都會復制整個數組,如果數組很大,會對內存造成壓力。
- 數據最終一致性: 讀操作讀到的是舊數組的數據,無法實時感知到其他線程剛寫入的最新數據。不適合對數據實時性要求很高的場景。
8. ConcurrentHashMap鏈表轉紅黑樹為什么是8?
這個設計是基于概率和統計,是時間和空間上的一個權衡。
- 目的: 為了解決哈希沖突嚴重時,鏈表過長導致的查詢性能從O(1)退化為O(n)的問題。紅黑樹是一種自平衡的二叉查找樹,查詢時間復雜度為O(log n)。
- 為什么是8?: 根據泊松分布的概率統計,在理想的哈希函數下,一個哈希桶中節點數量達到8的概率非常低(約為一千萬分之六)。這意味著,絕大多數情況下鏈表長度都不會超過8。選擇8這個閾值,可以保證在絕大多數情況下仍然使用鏈表這種更節省空間的結構,只有在極少數極端情況下,才會轉換為紅黑樹來保證性能。
- 樹退化為鏈表的閾值是6: 為什么不是7?這是為了避免在節點數量在8附近頻繁地轉換(比如一個節點頻繁地插入和刪除)。設置一個緩沖區間(6和8之間),可以有效防止因頻繁的增刪操作導致的不必要的樹化和退化,減少性能開銷。
9. 線程池用完以后是否需要shutdown嗎?
是的,強烈建議手動關閉。
如果不關閉,線程池中的核心線程會一直存活,阻止JVM的正常退出。
shutdown()
: 溫和的關閉。不再接受新任務,但會等待線程池中已有任務(包括正在執行的和在隊列中等待的)執行完畢。shutdownNow()
: 強制的關閉。嘗試中斷所有正在執行的任務,不再處理隊列中等待的任務,返回尚未執行的任務列表。
最佳實踐: 通常在應用程序結束時(例如通過JVM的shutdown hook),調用?shutdown()
?來優雅地關閉線程池。
10. Java中如何終止一個正在運行的線程?
停止一個線程的正確方式是“通知”它讓它自己停下來,而不是強制中斷它。
使用標志位(推薦): 設置一個 volatile 布爾類型的標志位,線程在運行時定期檢查這個標志。
public class MyThread extends Thread {private volatile boolean stopped = false;public void run() {while (!stopped) {// ... 執行任務}}public void stopGracefully() {this.stopped = true;} }
使用?
interrupt()
?方法: 這是一個協作機制。- 調用線程的?
interrupt()
?方法并不是強制終止線程,而是設置線程的中斷狀態為?true
。 - 被中斷的線程需要在自己的代碼中檢查中斷狀態并決定如何響應。
- 如果線程處于阻塞狀態(如?
sleep
,?wait
,?join
),它會拋出?InterruptedException
,并在捕獲異常后重置中斷狀態。 - 正確做法: 在任務代碼中捕獲?
InterruptedException
?或在循環中檢查?Thread.currentThread().isInterrupted()
。
public void run() {while (!Thread.currentThread().isInterrupted()) {try {// ... 執行任務,可能會調用sleep等阻塞方法} catch (InterruptedException e) {// 捕獲異常后,通常有兩種選擇:// 1. 重新設置中斷狀態,退出循環Thread.currentThread().interrupt();break;// 2. 直接退出循環break;}} }
- 調用線程的?
絕對不要使用被廢棄的?stop()
,?suspend()
,?resume()
?方法,因為它們會強制終止線程,立即釋放它持有的所有鎖,可能導致數據不一致性和死鎖問題。