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