# Java Memory Model ###### tags: `Java` `Thread` **Java Memory Model** JMM 決定了線程何時可以看到共享變量被其他線程的寫入 JMM定義了線程與記憶體間的抽象關係 1.共享變量儲存於記憶體內,所有線程皆可訪問 2.每個線程都有Local Memory 3.Local Memory只儲存主記憶體中共享變量的複本 4.線程無法直接讀寫主記憶體,需透過Local Memory 5.Local Memory跟JMM一樣都只是抽象的概念 ![](https://i.imgur.com/YrBwr7h.png) 當同一個數據同時被不同線程載入到Loacl Memory後 假設A線程在自己的Local Memory修改了數據後會同步更新到Main Memory 當B線程要使用該數據會發現該變量以失效,此時得再去Main Memory取得最新的數據 **線程與Local Memory與Main Memory交互方式** **原子性** 假設一個靜態變量為A 兩個線程同時對他賦值 Thread1:A=1; Thread2:A=2; 結果不是1就是2 但如果是 Thread1:A++; Thread2:A=2; A++的行為 A = A+1 這個操作可再細分為 讀取A->變數將變數+1->賦值 而CPU執行時"可能"Thread1執行到讀取後切到Thread2執行 導致有多種結果 *當一個操作只有全部完成或者是全部不完成才符合源子性* 在32位元的虛擬機 因long、double為64位存儲單元 因此可能造成讀到半個變量的問題 ps.商用虛擬機幾乎都把64位元的數據讀寫作為原子操作來執行,因此不用太在意這個問題 **可見性** 當兩個線程取得了Main Memory內的數據,其中一個線程修該改數據後 另外一個線程是否能立刻取得最新的數據 **有序性** Java保證單線程中的程式碼重排序後的結果是一致的 但在多線程中會有重排序的現象,重排序後指令與原指令的順序未必一致 指令重排有 1.編譯器優化重排 編譯器在不改變單線程語意的前提下可以重新安排程式碼的執行順序 2.指令並行重排 現今處理器採用了指令集並行技術將多條指令重疊執行 如果不存在數據倚賴性處理器可以改變程式碼對應的機器指令執行順序 3.記憶體系統重排 由於處理器使用緩存及讀寫緩衝區,這始得load與store操作看上去可能是 在亂序執行,因為三級緩存導致內存與緩存的數據同步有時間差 1屬於編譯器重排 2.3屬於處理器重排 重排優化可能造成可見性問題 編譯器重排 ![](https://i.imgur.com/N3KKfCN.png) 編譯器只保證單線程內語意不改變,因此以上圖片中Thread1、Thread2可能會出現重排序導致不同結果產生 處理器重排 處理器重排是對CPU性能的優化 以指令的執行角度來看,一條指令可分為多個步驟完成 IF:讀取指令 ID:指令解碼 EX:執行 MEM:記憶體存取 WB:寫回暫存器 而為了提高硬體使用率CPU指令是按造流水線技術來執行的 Instructon1 : IF ID EX MEM WB Instructon2 : IF ID EX MEM WB Instructon3 : IF ID EX MEM WB 如果沒有使用流水線技術的話,假設每個步驟執行時間為1ms 則第二條指令需等5ms後才開始執行,但使用的話則是1ms後就開始執行 雖然可以大大提升CPU效能,但當流水現出線中斷時所有的硬體皆會進入一輪停頓期,為了盡量阻止進入停頓期其中一種優化手段就是"指令重排" a = b + c ; d = e + f ; ----------- LW R1,b:IF ID EX MEM WB LW R2,c: IF ID EX MEM WB ADD R1,R2: IF ID X EX MEM WB SW a,R3: IF X ID EX MEM WB LW R4,e: X IF ID EX MEM WB LW R5,f: IF ID EX MEM WB ... 在執行到ADD R1,R2:時數R2尚未準備好 因此在從記憶體存取時就會進入停頓 因此這時候會進行指令重排 LW R1,b:IF ID EX MEM WB LW R2,c: IF ID EX MEM WB LW R4,e: IF ID EX MEM WB LW R5,f: IF ID EX MEM WB ADD R1,R2: IF ID EX MEM WB SW a,R3: IF ID EX MEM WB ... 這樣所有停頓就會消除了,而重排序對於單線程基本上不會有影響 但如果是多線程的話可能就會導致嚴重問題 class test{ int a = 0; boolean flag = false; public void writer(){ a = 1 ; flag = true; } public void read(){ if(flag){ int i = a + 1; } } } 在多線程環境下 A線程執行write() B線程執行read() A線程指令重排導致flag提早被賦值而CPU在a還未賦值時切換到B線程 此時就會造成髒讀 而JMM提供的解決方案 除了使用volatile、synchronized、Lock外還有定義一套happens-before原則 如果兩個操作的執行順序無法從happen-before推導出來,則他們無法保證有序性 **happens-before** 程式碼順序原則 : 在一個線程內程式碼按照編寫順序執行=>即使重排序後結果不會改變 鎖規則 : unlock一定發生於一個鎖的lock之後 volatile : 使用此修飾字修飾了一個變量時對於該變量寫的操作必定在讀操作之前 傳遞規則 : 如果操作A早於操作B而操作B又早於操作C 則操作A早於操作C 線程啟動規則 : Thread.start()必定早於線程內的任何操作 線程中段規則 : 對線程執行interrupt()方法肯定早於catch到中斷記號 線程結束規則 : 線程所有操作都要早於現成死亡之前 實例結束規則 : 一個對象的初始化必定早發生於finalize()