Java 并發模型基于 JVM 內存模型(JMM),資源管理涉及 IO、線程、鎖等關鍵組件。若對并發語義、資源生命周期理解不透徹,易引發死鎖、內存泄漏、數據錯亂等嚴重問題。
1. 并發三大特性(可見性、原子性、有序性)的破壞
可見性問題:多線程共享變量時,若未用
volatile
或同步機制,線程對變量的修改可能僅保存在工作內存,無法及時刷新到主內存,導致其他線程讀取舊值。class Flag {boolean stop = false; // 未用volatilevoid setStop() { stop = true; }boolean shouldStop() { return stop; } } // 線程1循環判斷shouldStop(),線程2調用setStop(),線程1可能永遠無法退出
原理:JMM 允許編譯器和 CPU 對指令重排序,
stop
變量缺少volatile
時,線程 1 可能緩存stop=false
,忽略線程 2 的修改。原子性破壞:
i++
等復合操作(讀取 - 修改 - 寫入)在多線程下非原子,可能導致結果錯誤:class Counter {int count = 0;void increment() { count++; } // 非原子操作 } // 1000線程并發調用,結果可能小于1000
解決方案:用
AtomicInteger
(CAS 機制)或synchronized
保證原子性。有序性問題:指令重排序可能打破代碼執行順序,典型案例是單例模式的雙重檢查鎖定(DCL):
class Singleton {private static Singleton instance; // 未用volatileprivate Singleton() {}static Singleton getInstance() {if (instance == null) { // 第一次檢查synchronized (Singleton.class) {if (instance == null) { // 第二次檢查instance = new Singleton(); // 可能重排序}}}return instance;} }
風險:
new Singleton()
可分解為 “分配內存→初始化對象→引用指向內存”,重排序后可能變為 “分配內存→引用指向內存→初始化對象”,導致其他線程獲取到未初始化的instance
。需用volatile
禁止重排序。
2. 線程池的參數配置與資源失控
核心參數的誤解:
corePoolSize
(核心線程數):線程池保留的最小線程數,若任務數超過此值,會放入工作隊列;maximumPoolSize
(最大線程數):隊列滿后允許創建的最大線程數,若超過此值,觸發拒絕策略。
錯誤配置案例:將corePoolSize
設為 0 且隊列容量極大,導致每次任務需重新創建線程,抵消線程池復用優勢。
拒絕策略的濫用:默認拒絕策略
AbortPolicy
會拋出RejectedExecutionException
,若未處理,可能導致任務丟失。高并發場景下,應根據業務選擇CallerRunsPolicy
(讓提交者線程執行,緩解壓力)或自定義策略。線程泄漏:任務執行時間過長、阻塞(如
BlockingQueue.take()
無超時)或拋出未捕獲異常,會導致線程池線程永久阻塞或終止,逐漸耗盡線程資源。
3. 鎖機制的高級陷阱
死鎖的隱蔽觸發:多線程交叉獲取鎖且不釋放,如線程 1 持有鎖 A 等待鎖 B,線程 2 持有鎖 B 等待鎖 A。更隱蔽的場景是 “鎖順序不一致”:不同方法獲取鎖的順序不同,在高并發下隨機觸發死鎖。
鎖升級與性能損耗:synchronized 鎖會從偏向鎖→輕量級鎖→重量級鎖升級,若頻繁競爭,會升級為重量級鎖(依賴 OS 互斥量),導致性能大幅下降。應避免在熱點代碼中使用 synchronized,或改用
ReentrantLock
(可中斷、公平鎖選擇)。鎖粒度不當:
- 過粗:對整個對象加鎖(如
public synchronized void method()
),導致無關操作阻塞; - 過細:過度拆分鎖(如每個字段加鎖),增加鎖競爭和上下文切換成本。
- 過粗:對整個對象加鎖(如
4. 資源泄漏的隱蔽形式
ThreadLocal 的內存泄漏:
ThreadLocal
通過Thread
的ThreadLocalMap
存儲線程私有變量,若ThreadLocal
對象被回收,ThreadLocalMap
中的Entry
(弱引用 key)會變為null
,但 value 仍被線程引用,若線程長期存活(如線程池核心線程),value 無法回收,導致內存泄漏。
解決:使用后調用ThreadLocal.remove()
清除 value。NIO 資源未釋放:
Selector
、SocketChannel
等 NIO 組件若未關閉,會導致文件描述符(FD)泄漏,最終觸發Too many open files
錯誤。尤其在網絡編程中,需確保close()
在finally
或 try-with-resources 中執行。數據庫連接池的連接泄漏:從連接池獲取
Connection
后未關閉(如conn.close()
被遺漏),會導致池內連接耗盡,后續請求阻塞。