JUC併發編程學習(十六)談談java內存模型JMM

JMM:java內存模型(Java Memory Model ),不存在的東西,一個概念,約定

在多線程環境下,線程之間要通信,就不得不提JMM(Java Memory Model )。

JMM是一種規範,目的是解決由於多線程通過共享內存進行通信時,存在的本地內存的數據不一致、編譯器會對代碼指令重排、處理器會對代碼亂序等帶來的問題。

內存劃分

JMM規定了內存主要劃分爲主內存和工作內存兩種。此處的主內存和工作內存跟JVM內存劃分(堆、棧、方法區)是在不同的層次上進行的。如果非要對應起來,主內存對應的是Java堆中的對象實例部分,工作內存對應的是棧中的部分區域,從更底層的來說,主內存對應的是物理內存,工作內存對應的是寄存器和高速緩存。

在這裏插入圖片描述
 JVM在設計時候考慮到,如果JAVA線程每次讀取和寫入變量都直接操作主內存,對性能影響比較大,所以每條線程擁有各自的工作內存,工作內存中的變量是主內存中的一份拷貝,線程對變量的讀取和寫入,直接在工作內存中操作,而不能直接去操作主內存中的變量。但是這樣就會出現一個問題,當一個線程修改了自己工作內存中變量,對其他線程是不可見的,會導致線程不安全的問題。因爲JMM制定了一套標準來保證開發者在編寫多線程程序的時候,能夠控制什麼時候內存會被同步給其他線程。

內存交互操作

Java 內存模型對主內存與工作內存之間的具體交互協議定義了八種操作,具體如下:

  • lock(鎖定):作用於主內存變量,把一個變量標識爲一條線程獨佔狀態。
  • unlock(解鎖):作用於主內存變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定。
  • read(讀取):作用於主內存變量,把一個變量從主內存傳輸到線程的工作內存中,以便隨後的 load 動作使用。
  • load(載入):作用於工作內存變量,把 read 操作從主內存中得到的變量值放入工作內存的變量副本中。
  • use(使用):作用於工作內存變量,把工作內存中的一個變量值傳遞給執行引擎,每當虛擬機遇到一個需要使用變量值的字節碼指令時執行此操作。
  • assign(賦值):作用於工作內存變量,把一個從執行引擎接收的值賦值給工作內存的變量,每當虛擬機遇到一個需要給變量進行賦值的字節碼指令時執行此操作。
  • store(存儲):作用於工作內存變量,把工作內存中一個變量的值傳遞到主內存中,以便後續 write 操作。
  • write(寫入):作用於主內存變量,把 store 操作從工作內存中得到的值放入主內存變量中。

在這裏插入圖片描述

JMM對這八種指令的使用,制定瞭如下規則:

  • 不允許read和load、store和write操作之一單獨出現。即使用了read必須load,使用了store必須write
  • 不允許線程丟棄他最近的assign操作,即工作變量的數據改變了之後,必須告知主存
  • 不允許一個線程將沒有assign的數據從工作內存同步回主內存
  • 一個新的變量必須在主內存中誕生,不允許工作內存直接使用一個未被初始化的變量。就是懟變量實施use、store操作之前,必須經過assign和load操作
  • 一個變量同一時間只有一個線程能對其進行lock。多次lock後,必須執行相同次數的unlock才能解鎖
  • 如果對一個變量進行lock操作,會清空所有工作內存中此變量的值,在執行引擎使用這個變量前,必須重新load或assign操作初始化變量的值
  • 如果一個變量沒有被lock,就不能對其進行unlock操作。也不能unlock一個被其他線程鎖住的變量
  • 對一個變量進行unlock操作之前,必須把此變量同步回主內存

關於JMM的一些同步的約定

  1. 線程解鎖前,必須把共享變量立刻刷回主內存。
  2. 線程加鎖前,必須讀取主存中的最新值到工作內存中
  3. 加鎖和解鎖是同一把鎖

Java內存模型帶來的問題

可見性問題

CPU中運行的線程從主存中拷貝共享對象obj到它的CPU緩存,把對象obj的count變量改爲2。但這個變更對運行在右邊CPU中的線程不可見,因爲這個更改還沒有flush到主存中:要解決共享對象可見性這個問題,我們可以使用java volatile關鍵字或者是加鎖

在這裏插入圖片描述

競爭現象(原子性)

線程A和線程B共享一個對象obj。假設線程A從主存讀取Obj.count變量到自己的CPU緩存,同時,線程B也讀取了Obj.count變量到它的CPU緩存,並且這兩個線程都對Obj.count做了加1操作。此時,Obj.count加1操作被執行了兩次,不過都在不同的CPU緩存中。如果這兩個加1操作是串行執行的,那麼Obj.count變量便會在原始值上加2,最終主存中的Obj.count的值會是3。然而下圖中兩個加1操作是並行的,不管是線程A還是線程B先flush計算結果到主存,最終主存中的Obj.count只會增加1次變成2,儘管一共有兩次加1操作。 要解決上面的問題我們可以使用java synchronized代碼塊。
在這裏插入圖片描述

重排序

在這裏插入圖片描述
在執行程序時,爲了提高性能,編譯器和處理器常常會對指令做重排序。因爲重排序而造成運行結果不同。

可查看jMM底層原理進行更深層的瞭解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章