編程核心原理)
1. 為什么我們需要內存模型在并發(fā)編程的世界里混亂似乎是常態(tài)。想象一下你正在指揮一個交響樂團每個樂手都按照自己的節(jié)奏演奏沒有統(tǒng)一的指揮棒結果必然是噪音而非音樂。JVM內存模型就是這個指揮棒它定義了多線程環(huán)境下內存訪問的規(guī)則。我曾在生產(chǎn)環(huán)境遇到過這樣一個案例一個看似簡單的計數(shù)器在多線程環(huán)境下出現(xiàn)了詭異的數(shù)值跳躍。單線程測試完美通過但上線后數(shù)據(jù)頻頻出錯。這就是典型的內存可見性問題——一個線程對變量的修改另一個線程可能永遠看不到。2. JVM內存模型的核心結構2.1 主內存與工作內存JVM內存模型將內存抽象為兩大區(qū)域主內存存儲所有共享變量工作內存每個線程私有的內存空間當線程需要讀取共享變量時必須先從主內存拷貝到工作內存寫入時也需要先在工作內存修改再刷新到主內存。這個設計帶來了性能提升但也埋下了并發(fā)問題的種子。2.2 內存間交互操作JVM定義了8種原子操作來控制內存交互lock鎖定unlock解鎖read讀取load載入use使用assign賦值store存儲write寫入這些操作必須按順序成對出現(xiàn)比如read之后必須loadstore之后必須write。但在實際執(zhí)行時編譯器可能會對這些指令進行重排序這就引出了我們的下一個重點。3. 指令重排序與內存屏障3.1 什么是指令重排序現(xiàn)代處理器和編譯器為了提高性能會對指令執(zhí)行順序進行優(yōu)化調整。比如int a 1; // 語句1 int b 2; // 語句2這兩條語句沒有依賴關系處理器可能會先執(zhí)行語句2再執(zhí)行語句1。3.2 內存屏障的作用內存屏障Memory Barrier就像交通警察確保某些操作必須按順序執(zhí)行。JVM主要使用四種屏障LoadLoad屏障StoreStore屏障LoadStore屏障StoreLoad屏障舉個例子StoreStore屏障確保在該屏障之前的所有store操作都先于之后的store操作完成。這在單例模式的雙重檢查鎖定中至關重要。4. Happens-Before原則詳解4.1 什么是Happens-BeforeHappens-Before是JVM定義的一個偏序關系如果操作A Happens-Before操作B那么A對內存的修改對B都是可見的。這不是時間上的先后關系而是可見性的保證。4.2 八大Happens-Before規(guī)則程序順序規(guī)則同一線程中的每個操作都happens-before于該線程中后續(xù)的每個操作監(jiān)視器鎖規(guī)則解鎖操作happens-before后續(xù)對同一鎖的加鎖操作volatile變量規(guī)則volatile變量的寫操作happens-before后續(xù)對該變量的讀操作線程啟動規(guī)則線程A啟動線程B那么A啟動B的操作happens-before于B的任何操作線程終止規(guī)則線程A等待線程B終止B的所有操作happens-before于A檢測到B終止中斷規(guī)則線程A調用B.interrupt() happens-before B檢測到中斷終結器規(guī)則對象構造函數(shù)結束happens-before該對象的finalize方法開始傳遞性如果A happens-before B且B happens-before C那么A happens-before C5. volatile關鍵字的深層原理5.1 volatile的語義volatile變量具有兩大特性可見性對volatile變量的寫操作會立即刷新到主內存禁止指令重排序編譯器不會對volatile變量的讀寫操作進行重排序5.2 volatile的內存語義實現(xiàn)當寫一個volatile變量時JVM會在寫操作后插入StoreStore屏障和StoreLoad屏障StoreStore屏障 volatile寫操作 StoreLoad屏障讀操作前會插入LoadLoad屏障和LoadStore屏障LoadLoad屏障 volatile讀操作 LoadStore屏障6. 鎖的內存語義6.1 鎖的獲取與釋放鎖的釋放和獲取實際上遵循與volatile相同的規(guī)則釋放鎖相當于寫volatile變量獲取鎖相當于讀volatile變量6.2 鎖的實現(xiàn)原理以ReentrantLock為例其內部使用AQSAbstractQueuedSynchronizer實現(xiàn)。當線程釋放鎖時會將state變量volatile修飾從1改為0這個寫操作會刷新線程工作內存中的所有變量到主內存。7. final域的內存語義7.1 final域的重排序規(guī)則對于final域編譯器和處理器需要遵守兩個重排序規(guī)則在構造函數(shù)內對final域的寫入與隨后把這個被構造對象的引用賦值給一個引用變量這兩個操作不能重排序初次讀包含final域的對象引用與隨后初次讀這個final域這兩個操作不能重排序7.2 final域的實現(xiàn)原理JVM會在final域的寫操作后插入StoreStore屏障確保final域的寫入在構造函數(shù)返回前對所有線程可見。8. 雙重檢查鎖定問題與解決方案8.1 經(jīng)典的雙重檢查鎖定問題public class Singleton { private static Singleton instance; public static Singleton getInstance() { if (instance null) { // 第一次檢查 synchronized (Singleton.class) { if (instance null) { // 第二次檢查 instance new Singleton(); } } } return instance; } }這段代碼看似完美但實際上存在嚴重問題。因為new Singleton()不是原子操作它包含分配內存空間初始化對象將引用指向分配的內存地址步驟2和3可能被重排序導致其他線程看到未完全初始化的對象。8.2 正確的實現(xiàn)方式解決方案是使用volatileprivate volatile static Singleton instance;volatile會禁止指令重排序確保對象的完全初始化對其他線程可見。9. 線程安全發(fā)布的最佳實踐9.1 安全發(fā)布的幾種方式通過靜態(tài)初始化器public static Holder holder new Holder(42);使用volatile或AtomicReference通過正確構造的final域通過鎖保護9.2 不可變對象的特殊規(guī)則不可變對象所有域都是final且正確構造可以被安全地發(fā)布即使沒有使用同步。這是因為final域的特殊內存語義保證了初始化安全性。10. 常見并發(fā)問題排查技巧10.1 內存可見性問題癥狀一個線程修改了共享變量但另一個線程看不到變化 解決方法使用volatile使用鎖同步使用原子變量10.2 指令重排序問題癥狀程序在單線程下正常多線程下出現(xiàn)異常 解決方法使用volatile使用final域使用鎖同步10.3 死鎖問題排查工具jstackJConsoleVisualVM預防措施按固定順序獲取鎖使用tryLock設置超時避免嵌套鎖11. JMM在實際項目中的應用案例11.1 高性能計數(shù)器使用AtomicLong比使用synchronized Long性能高得多因為避免了鎖的獲取和釋放開銷利用了CPU的CAS指令減少了線程上下文切換11.2 無鎖數(shù)據(jù)結構比如ConcurrentLinkedQueue它使用CAS操作實現(xiàn)線程安全比鎖的實現(xiàn)性能更高。但需要注意ABA問題可以通過AtomicStampedReference解決。11.3 線程池的任務提交當向線程池提交任務時需要確保任務對象的所有狀態(tài)對工作線程可見。通常使用volatile或鎖來保證這一點。12. JVM內存模型的演進12.1 Java 5之前的模型早期版本存在嚴重問題volatile的語義不夠明確導致不同JVM實現(xiàn)行為不一致。12.2 Java 5的JSR-133這個修訂解決了以下問題強化了volatile的語義明確了final的內存語義修復了雙重檢查鎖定問題12.3 后續(xù)版本的改進Java 9引入了VarHandle提供了更靈活的內存訪問方式Java 11進一步優(yōu)化了內存屏障的實現(xiàn)。13. 性能優(yōu)化建議13.1 減少同步范圍盡量縮小同步代碼塊的范圍比如// 不好 synchronized(this) { // 大量不相關的代碼 } // 好 synchronized(this) { // 只有需要同步的代碼 }13.2 使用讀寫鎖當讀多寫少時使用ReentrantReadWriteLock比普通鎖性能更好。13.3 考慮無鎖編程對于高性能場景可以考慮使用Atomic類LongAdderConcurrent集合14. 測試并發(fā)程序的技巧14.1 壓力測試使用工具如JMHJava Microbenchmark Harness進行基準測試注意預熱足夠次數(shù)考慮JIT編譯的影響多次運行取平均值14.2 確定性測試使用CountDownLatch或CyclicBarrier控制線程執(zhí)行順序重現(xiàn)并發(fā)問題。14.3 靜態(tài)分析工具使用FindBugs、SpotBugs等工具檢測潛在的并發(fā)問題。15. 其他語言的并發(fā)模型對比15.1 Go的CSP模型Go使用goroutine和channel實現(xiàn)并發(fā)避免了顯式鎖的使用降低了復雜度。15.2 Erlang的Actor模型每個Actor有自己的狀態(tài)通過消息傳遞通信天然避免了共享內存的問題。15.3 Rust的所有權模型Rust通過編譯時的所有權檢查完全避免了數(shù)據(jù)競爭但學習曲線較陡峭。16. 常見誤區(qū)與陷阱16.1 volatile能保證原子性錯誤volatile只能保證可見性和有序性不能保證復合操作的原子性。比如count需要額外的同步。16.2 final對象是不可變的錯誤final只保證引用不變對象內部狀態(tài)仍可能改變。要實現(xiàn)不可變所有域都必須是final且不可變。16.3 無鎖編程一定更快不一定。無鎖算法在低競爭時性能好但在高競爭時可能導致大量CAS失敗反而性能下降。17. 高級話題內存模型與緩存一致性17.1 MESI協(xié)議現(xiàn)代CPU使用MESIModified, Exclusive, Shared, Invalid協(xié)議維護緩存一致性。理解這一點有助于優(yōu)化內存訪問模式。17.2 偽共享問題當多個線程頻繁修改位于同一緩存行的不同變量時會導致性能下降。解決方法填充PaddingContended注解Java 817.3 內存屏障的硬件實現(xiàn)不同CPU架構x86, ARM對內存屏障的實現(xiàn)不同x86的TSOTotal Store Order模型相對嚴格而ARM更寬松。18. 工具與調試技巧18.1 JITWatch分析JIT編譯日志查看熱點方法和內聯(lián)決策。18.2 Java Object Layout (JOL)查看對象內存布局java -jar jol-cli.jar internals java.lang.String18.3 perf工具在Linux下使用perf統(tǒng)計緩存命中率、分支預測失敗等硬件事件。19. 設計并發(fā)程序的建議優(yōu)先使用現(xiàn)有的并發(fā)工具類如ConcurrentHashMap盡量縮小同步范圍考慮不可變性避免過早優(yōu)化編寫線程安全的文檔說明20. 個人經(jīng)驗分享在實際項目中我發(fā)現(xiàn)這些做法特別有用對共享狀態(tài)保持警惕默認認為所有共享變量都需要同步使用并發(fā)集合替代手動同步為并發(fā)代碼編寫更多的注釋解釋同步策略在代碼審查時特別關注并發(fā)問題保持同步策略簡單一致最深刻的教訓來自一個生產(chǎn)環(huán)境的死鎖問題兩個看似無關的模塊因為獲取鎖的順序不同在高負載時發(fā)生了死鎖?,F(xiàn)在我會在文檔中明確記錄所有鎖的獲取順序。