在未正確使用鎖的時候,多線程的程序可能變的很容易出錯,並且難以排查。而JMM則給我們一種規範,它描述了多線程程序如何與內存交互。
JMM大致描述:
JMM描述了線程如何與內存進行交互。Java虛擬機規範視圖定義一種Java內存模型,來屏蔽掉各種操作系統內存訪問的差異,以實現Java程序在各種平臺下都能達到一致的訪問效果。
JMM描述了JVM如何與計算機的內存進行交互
JMM都是圍繞著原子性,有序性和可見性進行展開的
JMM的主要目標是定義程序中各個變量的訪問規則,虛擬機將變量存儲到內存和從內存取出變量這樣的底層細節。此處的變量指在堆中存儲的元素。
多線程的時候為什麼容易出錯?
Java內存模型規定所有的共享變量都存儲在主內存中,而每條線程有自己的工作內存(本地內存),工作內存保存了共享變量的副本,而不同內存又無法訪問對方的工作內存,所以如果線程在工作內存中修改了變量副本,其它線程是無從得知的。
線程的傳值均需要通過主內存來完成
JMM模型
JMM模型
主內存與工作內存如何交互?
Java內存模型定義了8種操作來完成主內存與工作內存的交互細節,虛擬機必須保證這8種操作的每一個操作都是原子的,不可再分的。
lock: 作用於主內存的變量,把變量標識為線程獨佔的狀態
unlock: 與lock對應,把主內存中處於鎖定狀態的變量釋放出來,釋放後的變量才可以被其他線程鎖定。
read: 作用於主內存的變量,把一個變量的值從主內存傳輸到線程的工作內存,便於隨後的load使用。
load:作用於工作內存的變量,把read讀取到的變量放入工作內存副本
use: 作用於工作內存,把工作內存的變量值傳遞給執行引擎,每當虛擬機遇到一個需要使用到變量的值的字節碼指令時將會執行這個操作。
assign: 作用於工作內存,把執行引擎收到的值賦給工作內存的變量,虛擬機遇到賦值字節碼時候執行這個操作
store:作用於工作內存,把變量的值傳輸到住內存中,以便隨後的write使用
write:作用於主內存,把store操作從工作內存得到的值放入主內存的變量中。
JMM內存模型
執行上述8種基本操作的規則:
不允許read和load,store和write操作之一單獨出現。
不允許一個線程丟棄它最近的assign操作。即變量在工作內存中改變了賬號必須把變化同步回主內存
一個新的變量只允許在主內存中誕生,不允許工作內存直接使用未初始化的變量。
一個變量同一時刻只允許一條線程進行lock操作,但同一線程可以lock多次,lock多次之後必須執行同樣次數的unlock操作
如果對一個變量進行lock操作,那麼將會清空工作內存中此變量的值。
不允許對未lock的變量進行unlock操作,也不允許unlock一個被其它線程lock的變量
如果一個變量執行unlock操作,必須先把次變了同步回主內存中。
這8種操作定義相當嚴禁,實踐起來又比較麻煩,但是可以有助於我們理解多線程的工作原理。有一個與此8種操作相等的Happen-before原則。
Happen-before原則
這個是Java內存模型下無需任何同步器協助就已經存在,可以直接在編碼中使用。如果兩個操作之間的關係不在此列,並且無法從下列規則推導出來的話,它們的順序就沒有保障,虛擬機可以對他們進行任意的重排。
天然的happen-before
程序順序原則:一個線程內包裝語義的串行性
volatile變量的寫,先發生於讀,這保證了volatile變量的可見性
鎖規則:unlock先與lock
傳遞性:A 先於B,B先於C,那麼A必然先於C
線程的start先於線程的每一個動作
線程的所有操作優先於線程的終結(Thread.join())
線程的中斷(interupt)先於被中斷線程的代碼
對象的構造函數執行,先於finalize()方法
Java運行時數據區
JVM定義了一些程序運行時會使用到的運行時數據區,其中一些會隨著虛擬機啟動而創建,隨著虛擬機退出而銷燬。另外一些是與現場一一對應的,這些線程對應的數據區會隨著線程的開始和結束而創建和銷燬。
這部分參考JVM規範
1. pc寄存器
可以支持多條線程同時允許,每一條Java虛擬機線程都有自己的pc寄存器。任意時刻,一條JVM線程之後執行一個方法的代碼,這個方法被稱為當前方法(current method)
如果這個方法不是native的,那麼PC寄存器就保存JVM正在執行的字節碼指令地址。
如果是native的,那麼pc寄存器的值為undefined
pc寄存器的容量至少能保證一個returnAddress類型的數據或者一個平臺無關的本地指針的值。
2. JVM Stack(虛擬機棧)
每一個JVM線程都有自己的私有虛擬機棧,這個棧與線程同時創建,用於存儲棧幀(Frame)。
棧用來存儲局部變量與一些過程結果的地方。在方法調用和返回中也扮演了很重要的角色。
棧可以試固定分配的也可以動態調整
如果請求線程分配的容量超過JVM棧允許的最大容量,拋出StackOverflowError異常
如果JVM棧可以動態擴展,擴展的動作也已經嘗試過,但是沒有申請到足夠的內存,則拋出OutofMemoryError異常
3. Heap(堆)
堆是可以可供各個線程共享的運行時存儲區域,也是供所有類的實例和數組對象分配內存的區域。堆在JVM啟動的時候創建。
堆所存儲的就是被GC所管理的各種對象。
堆也是可以固定大小和動態調整的:
實際所需的堆超過的GC所提供的最大容量,那麼JVM拋出OutofMemoryError異常。
4. Method Area(方法區)
也是各個線程共享的運行時內存區,它存儲每一個類的實例信息,運行時常量池,字段和方法數據,構造函數和普通方法的字節碼等內容。還有一些特殊方法。
方法區是堆的邏輯組成部分,也在JVM啟動時創建,簡單的JVM可以不實現這個區域的垃圾收集。
方法區也可固定大小和動態分配與堆一樣,內存空間不夠,那麼JVM拋出OutofMemoryError異常。
5. Run-Time Constant Pool(運行時常量池)
在方法區中分配,在加載類和接口到虛擬機之後,就創建對應的運行時常量池。
它是class文件中每一個類或接口的常量池表的運行時表現形式。像字符串。Java的主要類型。
存儲區域不夠用時候拋出OutofMemoryError異常。
6. Native Method Stacks(原生方法棧或本地方法棧)
JDK中native的方法,System類和Thread類中有很多。使用C語言編寫的方法,這個也通常叫做C stack。
可以不支持本地方法棧,但是如果支持的時候,這個棧一般會在線程創建的時候按線程分配。
與棧的錯誤一樣,StackOverFlowError和OutOfMemeoryError.
一個案例
案例
一個本地變量可能是原始類型,在這種情況下,它總是“呆在”線程棧上。
一個本地變量也可能是指向一個對象的一個引用。在這種情況下,引用(這個本地變量)存放在線程棧上,但是對象本身存放在堆上。
一個對象可能包含方法,這些方法可能包含本地變量。這些本地變量任然存放在線程棧上,即使這些方法所屬的對象存放在堆上。
一個對象的成員變量可能隨著這個對象自身存放在堆上。不管這個成員變量是原始類型還是引用類型。
靜態成員變量跟隨著類定義一起也存放在堆上。
存放在堆上的對象可以被所有持有對這個對象引用的線程訪問。當一個線程可以訪問一個對象時,它也可以訪問這個對象的成員變量。如果兩個線程同時調用同一個對象上的同一個方法,它們將會都訪問這個對象的成員變量,但是每一個線程都擁有這個本地變量的私有拷貝。
Java內存模型和硬件內存架構之間的對應
最後
這次主要講了一些規則相關的東西,及Java中運行時數據存儲的位置,建議看一下《深入理解JVM》最後一章。最好下載JSR-133規範對照著看。
參考:
-
Java內存模型
《深入理解Java虛擬機》
《Java高併發程序設計》
《JVM specification》
來源:簡書,著作權歸作者所有(https://www.jianshu.com/p/a463e23aedd5)。
閱讀更多 3T教育編程猿 的文章