您好,登錄后才能下訂單哦!
這期內容當中小編將會給大家帶來有關怎么深入理解Java內存模型JMM,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
Java 內存模型(JMM)是一種抽象的概念,并不真實存在,它描述了一組規則或規范,通過這組規范定義了程序中各個變量(包括實例字段、靜態字段和構成數組對象的元素)的訪問方式。試圖屏蔽各種硬件和操作系統的內存訪問差異,以實現讓 Java 程序在各種平臺下都能達到一致的內存訪問效果。
注意JMM與JVM內存區域劃分的區別:
JMM描述的是一組規則,圍繞原子性、有序性和可見性展開;
相似點:存在共享區域和私有區域
處理器上的寄存器的讀寫的速度比內存快幾個數量級,為了解決這種速度矛盾,在它們之間加入了高速緩存。
加入高速緩存帶來了一個新的問題:緩存一致性。如果多個緩存共享同一塊主內存區域,那么多個緩存的數據可能會不一致,需要一些協議來解決這個問題。
所有的變量都 存儲在主內存中,每個線程還有自己的工作內存 ,工作內存存儲在高速緩存或者寄存器中,保存了該線程使用的變量的主內存副本拷貝。
線程只能直接操作工作內存中的變量,不同線程之間的變量值傳遞需要通過主內存來完成。
方法中的基本類型本地變量將直接存儲在工作內存的棧幀結構中;
引用類型的本地變量:引用存儲在工作內存,實際存儲在主內存;
成員變量、靜態變量、類信息均會被存儲在主內存中;
主內存共享的方式是線程各拷貝一份數據到工作內存中,操作完成后就刷新到主內存中。
Java 內存模型定義了 8 個操作來完成主內存和工作內存的交互操作。
read:把一個變量的值從主內存傳輸到工作內存中
load:在 read 之后執行,把 read 得到的值放入工作內存的變量副本中
use:把工作內存中一個變量的值傳遞給執行引擎
assign:把一個從執行引擎接收到的值賦給工作內存的變量
store:把工作內存的一個變量的值傳送到主內存中
write:在 store 之后執行,把 store 得到的值放入主內存的變量中
lock:作用于主內存的變量
unlock
在單線程環境下不能改變程序的運行結果;
存在數據依賴關系的不允許重排序;
無法通過Happens-before原則推到出來的,才能進行指令的重排序。
Java 內存模型保證了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如對一個 int 類型的變量執行 assign 賦值操作,這個操作就是原子性的。但是 Java 內存模型允許虛擬機將沒有被 volatile 修飾的 64 位數據(long,double)的讀寫操作劃分為兩次 32 位的操作來進行,即 load、store、read 和 write 操作可以不具備原子性。
有一個錯誤認識就是,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 規則 。與此同時,更多復雜度在于,需要盡量確保各種編譯器、各種體系結構的處理器,都能夠提供一致的行為。
主要有有三種實現可見性的方式:
volatile,會 強制 將該變量自己和當時其他變量的狀態都 刷出緩存 。
synchronized,對一個變量執行 unlock 操作之前,必須把變量值同步回主內存。
final,被 final 關鍵字修飾的字段在構造器中一旦初始化完成,并且沒有發生 this 逃逸(其它線程通過 this 引用訪問到初始化了一半的對象),那么其它線程就能看見 final 字段的值。
對前面的線程不安全示例中的 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。
上述就是小編為大家分享的怎么深入理解Java內存模型JMM了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。