Java內存模型

 閱讀大約需要1分鐘

Java內存模型

Java內存模型(Java Memory Model,JMM)就是一種符合內存模型規範的,屏蔽了各種硬件和操作系統的訪問差異的,保證了Java程序在各種平台下對內存的訪問都能保證效果一致的機制及規範。

簡要言之,jmm是jvm的一種規範,定義了jvm的內存模型。它屏蔽了各種硬件和操作系統的訪問差異,不像c那樣直接訪問硬件內存,相對安全很多,
它的主要目的是解決由於多線程通過共享內存進行通信時,存在的本地內存數據不一致、編譯器會對代碼指令重排序、處理器會對代碼亂序執行等帶來的問題,
可以保證並發編程場景中的原子性、可見性和有序性。

原子性:每一種操作都是原子的、不可再分的

可見性:是指當一個線程修改了某一個共享變量的值,其他線程是否能夠立即知道該變更,JMM規定了所有的變量都存儲在主內存中。

有序性:支持指令重排。

這裡對有序性進行詳細講解

什麼是有序性?

對於一個線程的執行代碼而言,我們總是習慣認為代碼的執行總是從上到下,有序執行。 但為了提升性能,編譯器和處理器通常會對指令序列進行 重新排序。 Java規範規定JVM線程內部維持順序化語義,即只要程序的最終結果與它順序化執行的結果相等,那麼指令的執行順序可以與代碼順序不一致,此過程叫指令的重排序。

有序性的優缺點

JVM能根據處理器特性(CPU多級緩存系統、多核處理器等)適當的對機器指令進行重排序,使機器指令能更符合CPU的執行特性,最大限度的發揮機器性能。 但是指令重排可以保證串行語義一致,但沒有義務保證多線程間的語義也一致(即可能產生"臟讀"),簡單說,兩行以上不相干的代碼在執行的時候有可能先執行的不是第一條,不見得是從上到下順序執行,執行順序會被優化。

單線程環境裡面確保程序最終執行結果和代碼順序執行的結果一致。

處理器在進行重排序時必須要考慮指令之間的數據依賴性。

多線程環境中線程交替執行,由於編譯器優化重排的存在,兩個線程中使用的變量能否保證一致性是無法確定的,結果無法預測。

Java內存模型規定所有的變量都存儲在主內存中,每條線程還有自己的工作內存, 線程的工作內存中保存了被該線程使用到的變量的主內存副本的拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行, 而不能直接讀寫主內存中的變量。不同的線程之間也無法訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成。

主內存:主要對應Java堆中的對象實例數據部分。(寄存器,高速緩存)

工作內存:對應於虛擬機棧中的部分區域。(硬件的內存)

關於主內存與工作內存之間具體的交互協議,即一個變量如何從主內存拷貝到工作內存、如何從工作內存同步回主內存這一類的實現細節。 Java虛擬機實現時必須保證下面提及的每一種操作都是原子的、不可再分的:

Lock(鎖定):作用於主內存的變量,它把一個變量標識為一條線程獨占的狀態。

unLock(解鎖):作用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才可以被其他線程所定。

Read(讀取):作用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的 load動作使用。

Load(載入):作用於工作內存的變量,它把 read操作從主內存中得到的變量值放入工作內存的變量副本中。

Use(使用):作用於工作內存的變量,它把工作內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用變量的值的字節碼指令時將會執行這個操作。

Assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收的值符給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。

Store(存儲):作用於工作內存的變量,它把工作內存中一個變量的值傳送到主內存中,以便隨後的write操作使用。

Write(寫入):作用於主內存的變量,它把 store 操作從工作內存中得到的變量的值放入主內存的變量中。

  • 如果對一個變量執行lock操作,將會清空工作內存中此變量的值;
  • 對一個變量執行 unlock 操作之前,必須先把此變量同步到主內存中。
內存屏障(Memory Barrier)

硬件層的內存屏障分為兩種:Load Barrier和Store Barrier即讀屏障和寫屏障(內存屏障是硬件層的)。

為什麼需要內存屏障

由於現代操作系統都是多處理器操作系統,每個處理器都會有自己的緩存,可能存在不同處理器緩存不一致的問題, 而且由於操作系統可能存在重排序,導致讀取到錯誤的數據,因此操作系統提供了一些內存屏障以解決這種問題。

簡單來說:

  1. 在不同CPU執行的不同線程對同一個變量的緩存值不同

  2. 用volatile可以解決上面的問題,不同硬件對內存屏障的實現方式不一樣。 java屏蔽掉這些差異,通過jvm生成內存屏障的指令。 對於讀屏障:在指令前插入讀屏障,可以讓高速緩存中的數據失效,強制從主內存取。

內存屏障的作用

cpu執行指令可能是無序的,它有兩個比較重要的作用

  1. 阻止屏障兩側指令重排序
  2. 強制把寫緩衝區/高速緩存中的髒數據等寫回主內存,讓緩存中相應的數據失效。
volatile型變量

當我們聲明某個變量為volatile修飾時,這個變量就有了線程可見性,volatile通過在讀寫操作前後添加內存屏障。

volatile型變量擁有如下特性

  1. 可見性,對於一個該變量的讀,一定能看到讀之前最後的寫入。
  2. 防止指令重排序,執行代碼時,為了提高執行效率,會在不影響最後結果的前提下對指令進行重新排序,使用volatile可以防止, 比如單例模式雙重校驗鎖的創建中有使用到

注意的是volatile不具有原子性

至於volatile底層是怎麼實現保證不同線程可見性的,這裡涉及到的就是硬件上的, 被volatile修飾的變量在進行寫操作時,會生成一個特殊的彙編指令,該指令會觸發mesi協議, 會存在一個總線嗅探機制的東西,簡單來說就是這個cpu會不停檢測總線中該變量的變化, 如果該變量一旦變化了,由於這個嗅探機制,其它cpu會立馬將該變量的cpu緩存數據清空掉, 重新的去從主內存拿到這個數據。

多線程先行發生原則之happens-before

Java語言中JMM原則下有一個“先行發生”(happens-before)的原則,這個原則非常重要:

它是判斷數據是否存在競爭,線程是否安全的非常有用的手段。依賴這個原則,我們可以通過幾條簡單規矩一攬子解決並發環境下兩個操作之間是否可能存在衝突的所有問題,而不需要陷入Java內存模型苦澀難懂的底層編譯原理之中。

在JMM中,如果一個操作執行的結果需要對另一個操作可見性 或者 代碼重排序,那麼這兩個操作之間必須存在 happens-before(先行發生)原則

happens-before原則簡而言之就是:

如果一個操作先行發生於另一個操作,那麼第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行結果順序排在第二個操作之前。

兩個操作之間存在happens-before關係,並不意味著一定要按照happens-before原則制定的順序來執行。如果重排序之後的執行結果與按照happens-before關係來執行的結果一致,那麼這種排序並不非法。

happens-before有以下八條原則:

次序規則:一個線程內,按照代碼的順序,寫在前面的操作先行發生於寫在後面的操作

鎖定規則:一個unLock操作先行發生於後面(“後面”是指時間上的先後)對同一個鎖的lock操作

volatile變量規則:對一個volatile變量的寫操作先行發生於後面對這個變量的讀操作,前面的寫對後面的讀是可見的(“後面”是指時間上的先後)

傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C

線程啟動規則(Thread Start Rule):Thread對象的start()方法先行發生於此線程的每一個動作

線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷時間的發生,即先調用interrupt()方法設置過中斷標誌位,才能通過Thread.interrupted()檢測到是否發生中斷。

線程終止規則(Thread Termination Rule):線程中的所有操作都先行發生於對此線程的終止檢測,我們可以通過isAlive()等手段檢測線程是否已經終止執行。

對象終結規則(Finalizer Rule):一個對象的初始化完成(構造函數執行結束)先行發生於它的finalize()方法的開始。