Java 內存模型(Java Memory Model,JMM)看上去和 Java 內存結構(JVM 運行時內存結構)差很少,但這二者並非一回事。JMM 並不像 JVM 內存結構同樣是真實存在的,它只是一個抽象的概念。html
Java 的線程間經過共享內存(Java堆和方法區)進行通訊,在通訊過程當中會存在一系列如可見性、原子性、順序性等問題,而 JMM 就是圍繞着多線程通訊以及與其相關的一系列特性而創建的模型。java
如今說 JMM 通常指的是 JDK 5 開始使用的新的內存模型,主要由 JSR-133: JavaTM Memory Model and Thread Specification(http://www.cs.umd.edu/users/pugh/java/memoryModel/,http://ifeve.com/jsr133-cn/)描述。程序員
一個操做不可被中斷,要麼執行完成,要麼就不執行。多線程
public static int k = 0; // 多線程去操做一個共享變量,多運行幾回,能夠看到不同的結果 public static void main(String[] args) throws InterruptedException { ThreadPoolExecutor tpe = new ThreadPoolExecutor(10, 10, 0, TimeUnit.MICROSECONDS, new LinkedBlockingQueue<>(1000)); for (int i = 0; i < 1000; i++) { tpe.execute(() -> { k++; }); } Thread.sleep(1000); System.out.println(k); tpe.shutdown(); }
多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看到修改的值。併發
private static boolean flag = false; // 多運行幾回,會出現沒法結束的狀況 public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (true) { if (flag) { System.out.println("------------------"); break; } } }).start(); // Thread.sleep(100); new Thread(() -> { flag = true; }).start(); }
程序執行的順序按照代碼的前後順序執行。app
編譯器和處理器會對指令進行重排序來達到優化效果,重排序後不會影響單線程執行的結果(as-if-serial),但可能會影響多線程併發執行的結果。ide
爲了保證共享內存的正確性(可見性、有序性、原子性),JMM 定義了共享內存系統中多線程程序讀寫操做行爲的規範。優化
JMM 解決併發問題主要採用兩種方式:限制處理器優化和使用內存屏障。spa
Java 內存模型規定了全部的變量都存儲在主內存中,每條線程還有本身的工做內存,用於存儲主存中要使用的變量的副本。線程
線程對變量的全部操做都必須在工做內存中進行,而不能直接讀寫主內存。
不一樣的線程之間沒法直接訪問對方工做內存中的變量。
線程間變量的傳遞須要本身的工做內存和主存之間進行數據同步。
Java 內存模型定義了八種操做來完成。
在 32 位計算機上,對於 long double 64位讀取,須要進行兩次處理,非原子性操做。在 64 位上無問題。
lock(鎖定):做用於主內存的變量,把一個變量標識爲一條線程獨佔狀態。 unlock(解鎖):做用於主內存變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定。 read(讀取):做用於主內存變量,把一個變量值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用 load(載入):做用於工做內存的變量,它把read操做從主內存中獲得的變量值放入工做內存的變量副本中。 use(使用):做用於工做內存的變量,把工做內存中的一個變量值傳遞給執行引擎,每當虛擬機遇到一個須要使用變量的值的字節碼指令時將會執行這個操做。 assign(賦值):做用於工做內存的變量,它把一個從執行引擎接收到的值賦值給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做。 store(存儲):做用於工做內存的變量,把工做內存中的一個變量的值傳送到主內存中,以便隨後的write的操做。 write(寫入):做用於主內存的變量,它把store操做從工做內存中一個變量的值傳送到主內存的變量中。
在JMM中,若是一個操做執行的結果須要對另外一個操做可見(並不意味着前一個操做必需要在後一個操做以前執行),那麼這兩個操做之間必須存在 happens-before 關係。
happens-before 的八大原則:
1.程序次序規則:一個線程內,按照代碼順序,書寫在前面的操做先行發生於書寫在後面的操做; 2.鎖定規則:一個unLock操做先行發生於後面對同一個鎖的lock操做; 3.volatile變量規則:對一個變量的寫操做先行發生於後面對這個變量的讀操做; 4.傳遞規則:若是操做A先行發生於操做B,而操做B又先行發生於操做C,則能夠得出操做A先行發生於操做C; 5.線程啓動規則:Thread對象的start()方法先行發生於此線程的每個動做; 6.線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生; 7.線程終結規則:線程中全部的操做都先行發生於線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive0的返回值手段檢測到線程已經終止執行; 8.對象終結規則:一個對象的初始化完成先行發生於他的finalize)方法的開始;
若是兩個操做不知足上述任意一個 happens-before 規則,那麼這兩個操做就沒有順序的保障,JVM能夠對這兩個操做進行重排序。若是操做A happens-before 操做 B,那麼操做 A 在內存上所作的操做對操做 B 都是可見的。
Java 內存模型,除了定義了一套規範,還提供了一系列語義,封裝了底層實現後,供開發者直接使用。
如 volatile、synchronized、final、concurren 包等。這些就是 Java 內存模型封裝了底層的實現後提供給程序員使用的一些關鍵字。
給關鍵代碼加鎖
public static int k = 0; public static void main(String[] args) throws InterruptedException { ThreadPoolExecutor tpe = new ThreadPoolExecutor(10, 10, 0, TimeUnit.MICROSECONDS, new LinkedBlockingQueue<>(1000)); for (int i = 0; i < 1000; i++) { tpe.execute(() -> { synchronized (tpe) { k++; } }); } Thread.sleep(1000); System.out.println(k); tpe.shutdown(); }
用 concurren 包中的原子變量代替基本變量
public static AtomicInteger k = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { ThreadPoolExecutor tpe = new ThreadPoolExecutor(10, 10, 0, TimeUnit.MICROSECONDS, new LinkedBlockingQueue<>(1000)); for (int i = 0; i < 1000; i++) { tpe.execute(() -> { k.incrementAndGet(); }); } Thread.sleep(1000); System.out.println(k); tpe.shutdown(); }
給變量加上 volatile 修飾
private static volatile boolean flag = false; public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (true) { if (flag) { System.out.println("------------------"); break; } } }).start(); Thread.sleep(100); new Thread(() -> { flag = true; }).start(); }
使用 synchronized 和 volatile 來保證多線程之間操做的有序性。
volatile 關鍵字會禁止指令重排。synchronized 關鍵字保證同一時刻只容許一條線程操做。
https://www.hollischuang.com/archives/2509