Java 內存模型(JMM)是一種抽象的概念,並不真實存在,它描述了一組規則或規範,經過這組規範定義了程序中各個變量(包括實例字段、靜態字段和構成數組對象的元素)的訪問方式。試圖屏蔽各類硬件和操做系統的內存訪問差別,以實現讓 Java 程序在各類平臺下都能達到一致的內存訪問效果。程序員
注意JMM與JVM內存區域劃分的區別:數組
處理器上的寄存器的讀寫的速度比內存快幾個數量級,爲了解決這種速度矛盾,在它們之間加入了高速緩存。緩存
加入高速緩存帶來了一個新的問題:緩存一致性。若是多個緩存共享同一塊主內存區域,那麼多個緩存的數據可能會不一致,須要一些協議來解決這個問題。安全
全部的變量都 存儲在主內存中,每一個線程還有本身的工做內存 ,工做內存存儲在高速緩存或者寄存器中,保存了該線程使用的變量的主內存副本拷貝。多線程
線程只能直接操做工做內存中的變量,不一樣線程之間的變量值傳遞須要經過主內存來完成。架構
Java 內存模型定義了 8 個操做來完成主內存和工做內存的交互操做。併發
Java 內存模型保證了 read、load、use、assign、store、write、lock 和 unlock 操做具備原子性,例如對一個 int 類型的變量執行 assign 賦值操做,這個操做就是原子性的。可是 Java 內存模型容許虛擬機將沒有被 volatile 修飾的 64 位數據(long,double)的讀寫操做劃分爲兩次 32 位的操做來進行,即 load、store、read 和 write 操做能夠不具有原子性。app
有一個錯誤認識就是,int 等原子性的類型在多線程環境中不會出現線程安全問題。前面的線程不安全示例代碼中,cnt 屬於 int 類型變量,1000 個線程對它進行自增操做以後,獲得的值爲 997 而不是 1000。分佈式
爲了方便討論,將內存間的交互操做簡化爲 3 個:load、assign、store。函數
下圖演示了兩個線程同時對 cnt 進行操做,load、assign、store 這一系列操做總體上看不具有原子性,那麼在 T1 修改 cnt 而且尚未將修改後的值寫入主內存,T2 依然能夠讀入舊值。能夠看出,這兩個線程雖然執行了兩次自增運算,可是主內存中 cnt 的值最後爲 1 而不是 2。所以對 int 類型讀寫操做知足原子性只是說明 load、assign、store 這些單個操做具有原子性。
AtomicInteger 能保證多個線程修改的原子性。
使用 AtomicInteger 重寫以前線程不安全的代碼以後獲得如下線程安全實現:
public class AtomicExample { private AtomicInteger cnt = new AtomicInteger(); public void add() { cnt.incrementAndGet(); } public int get() { return cnt.get(); } }複製代碼
public static void main(String[] args) throws InterruptedException { final int threadSize = 1000; AtomicExample example = new AtomicExample(); // 只修改這條語句 final CountDownLatch countDownLatch = new CountDownLatch(threadSize); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < threadSize; i++) { executorService.execute(() -> { example.add(); countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println(example.get()); }複製代碼
1000複製代碼
除了使用原子類以外,也可使用 synchronized 互斥鎖來保證操做的原子性。它對應的內存間交互操做爲:lock 和 unlock,在虛擬機實現上對應的字節碼指令爲 monitorenter 和 monitorexit。
public class AtomicSynchronizedExample { private int cnt = 0; public synchronized void add() { cnt++; } public synchronized int get() { return cnt; } }複製代碼
public static void main(String[] args) throws InterruptedException { final int threadSize = 1000; AtomicSynchronizedExample example = new AtomicSynchronizedExample(); final CountDownLatch countDownLatch = new CountDownLatch(threadSize); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < threadSize; i++) { executorService.execute(() -> { example.add(); countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println(example.get()); }複製代碼
1000複製代碼
可見性指當一個線程修改了共享變量的值,其它線程可以當即得知這個修改。Java 內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值來實現可見性的。JMM 內部的實現一般是依賴於所謂的 內存屏障 ,經過 禁止某些重排序 的方式,提供內存 可見性保證 ,也就是實現了 各類 happen-before 規則 。與此同時,更多複雜度在於,須要儘可能確保各類編譯器、各類體系結構的處理器,都可以提供一致的行爲。
主要有有三種實現可見性的方式:
對前面的線程不安全示例中的 cnt 變量使用 volatile 修飾,不能解決線程不安全問題,由於 volatile 並不能保證操做的原子性。
有序性是指:在本線程內觀察,全部操做都是有序的。在一個線程觀察另外一個線程,全部操做都是無序的,無序是由於發生了指令重排序。在 Java 內存模型中,容許編譯器和處理器對指令進行重排序,重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。
volatile 關鍵字經過添加內存屏障的方式來禁止指令重排,即重排序時不能把後面的指令放到內存屏障以前。
也能夠經過 synchronized 來保證有序性,它保證每一個時刻只有一個線程執行同步代碼,至關因而讓線程順序執行同步代碼。
JSR-133內存模型使用先行發生原則在Java內存模型中保證多線程操做 可見性 的機制,也是對早期語言規範中含糊的可見性概念的一個精肯定義。上面提到了能夠用 volatile 和 synchronized 來保證有序性。除此以外,JVM 還規定了先行發生原則,讓一個操做 無需控制 就能先於另外一個操做完成。
因爲 指令重排序 的存在,兩個操做之間有happen-before關係, 並不意味着前一個操做必需要在後一個操做以前執行。 僅僅要求前一個操做的執行結果對於後一個操做是可見的,而且前一個操做 按順序 排在第二個操做以前。
Single Thread rule
在一個線程內,在程序前面的操做先行發生於後面的操做。
Monitor Lock Rule
一個 unlock(解鎖) 操做 先行發生於 後面對同一個鎖的 lock(加鎖) 操做。
Volatile Variable Rule
對一個 volatile 變量的 寫操做 先行發生於後面對這個變量的 讀操做 。
Thread Start Rule
Thread 對象的 start() 方法調用先行發生於此線程的每個動做。
Thread Join Rule
Thread 對象的結束先行發生於 join() 方法返回。
Thread Interruption Rule
對線程 interrupt() 方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,能夠經過 interrupted() 方法檢測到是否有中斷髮生。
Finalizer Rule
一個對象的初始化完成(構造函數執行結束)先行發生於它的 finalize() 方法的開始。
Transitivity
若是操做 A 先行發生於操做 B,操做 B 先行發生於操做 C,那麼操做 A 先行發生於操做 C。