java内存模型JMM理解整理
?
什么是JMM
JMM即為JAVA 內存模型(java memory model)。因為在不同的硬件生產商和不同的操作系統下,內存的訪問邏輯有一定的差異,結果就是當你的代碼在某個系統環境下運行良好,并且線程安全,但是換了個系統就出現各種問題。Java內存模型,就是為了屏蔽系統和硬件的差異,讓一套代碼在不同平臺下能到達相同的訪問結果。JMM從java 5開始的JSR-133發布后,已經成熟和完善起來。
內存劃分
JMM規定了內存主要劃分為主內存和工作內存兩種。此處的主內存和工作內存跟JVM內存劃分(堆、棧、方法區)是在不同的層次上進行的,如果非要對應起來,主內存對應的是Java堆中的對象實例部分,工作內存對應的是棧中的部分區域,從更底層的來說,主內存對應的是硬件的物理內存,工作內存對應的是寄存器和高速緩存。
JVM在設計時候考慮到,如果JAVA線程每次讀取和寫入變量都直接操作主內存,對性能影響比較大,所以每條線程擁有各自的工作內存,工作內存中的變量是主內存中的一份拷貝,線程對變量的讀取和寫入,直接在工作內存中操作,而不能直接去操作主內存中的變量。但是這樣就會出現一個問題,當一個線程修改了自己工作內存中變量,對其他線程是不可見的,會導致線程不安全的問題。因為JMM制定了一套標準來保證開發者在編寫多線程程序的時候,能夠控制什么時候內存會被同步給其他線程。
內存交互操作
? 內存交互操作有8種,虛擬機實現必須保證每一個操作都是原子的,不可在分的(對于double和long類型的變量來說,load、store、read和write操作在某些平臺上允許例外)
-
- 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對這八種操作規則和對volatile的一些特殊規則就能確定哪里操作是線程安全,哪些操作是線程不安全的了。但是這些規則實在復雜,很難在實踐中直接分析。所以一般我們也不會通過上述規則進行分析。更多的時候,使用java的happen-before規則來進行分析。
模型特征
原子性:例如上面八項操作,在操作系統里面是不可分割的單元。被synchronized關鍵字或其他鎖包裹起來的操作也可以認為是原子的。從一個線程觀察另外一個線程的時候,看到的都是一個個原子性的操作。
1 synchronized (this) { 2 a=1; 3 b=2; 4 }例如一個線程觀察另外一個線程執行上面的代碼,只能看到a、b都被賦值成功結果,或者a、b都尚未被賦值的結果。
可見性:每個工作線程都有自己的工作內存,所以當某個線程修改完某個變量之后,在其他的線程中,未必能觀察到該變量已經被修改。volatile關鍵字要求被修改之后的變量要求立即更新到主內存,每次使用前從主內存處進行讀取。因此volatile可以保證可見性。除了volatile以外,synchronized和final也能實現可見性。synchronized保證unlock之前必須先把變量刷新回主內存。final修飾的字段在構造器中一旦完成初始化,并且構造器沒有this逸出,那么其他線程就能看到final字段的值。
有序性:java的有序性跟線程相關。如果在線程內部觀察,會發現當前線程的一切操作都是有序的。如果在線程的外部來觀察的話,會發現線程的所有操作都是無序的。因為JMM的工作內存和主內存之間存在延遲,而且java會對一些指令進行重新排序。volatile和synchronized可以保證程序的有序性,很多程序員只理解這兩個關鍵字的執行互斥,而沒有很好的理解到volatile和synchronized也能保證指令不進行重排序。
Volatile內存語義
volatile的一些特殊規則
Final域的內存語義
被final修飾的變量,相比普通變量,內存語義有一些不同。具體如下:
-
- JMM禁止把Final域的寫重排序到構造器的外部。
- 在一個線程中,初次讀該對象和讀該對象下的Final域,JMM禁止處理器重新排序這兩個操作。
假設現在有線程A執行FinalConstructor.write()方法,線程B執行FinalConstructor.read()方法。
對應上述的Final的第一條規則,因為JMM禁止把Final域的寫重排序到構造器的外部,而對普通變量沒有這種限制,所以變量A=1,而變量B可能會等于2(構造完成),也有可能等于0(第11行代碼被重排序到構造器的外部)。
? 對應上述的Final的第二條規則,如果constructor的引用不為null,A必然為1,要么constructor為null,拋出空指針異常。保證讀final域之前,一定會先讀該對象的引用。但是普通對象就沒有這種規則。
(上述的Final規則反復測試,遺憾的是我并沒有能模擬出來普通變量不能正常構造的結果)
Happen-Before(先行發生規則)
在常規的開發中,如果我們通過上述規則來分析一個并發程序是否安全,估計腦殼會很疼。因為更多時候,我們是分析一個并發程序是否安全,其實都依賴Happen-Before原則進行分析。Happen-Before被翻譯成先行發生原則,意思就是當A操作先行發生于B操作,則在發生B操作的時候,操作A產生的影響能被B觀察到,“影響”包括修改了內存中的共享變量的值、發送了消息、調用了方法等。
Happen-Before的規則有以下幾條
-
- 程序次序規則(Program Order Rule):在一個線程內,程序的執行規則跟程序的書寫規則是一致的,從上往下執行。
- 管程鎖定規則(Monitor Lock Rule):一個Unlock的操作肯定先于下一次Lock的操作。這里必須是同一個鎖。同理我們可以認為在synchronized同步同一個鎖的時候,鎖內先行執行的代碼,對后續同步該鎖的線程來說是完全可見的。
- volatile變量規則(volatile Variable Rule):對同一個volatile的變量,先行發生的寫操作,肯定早于后續發生的讀操作
- 線程啟動規則(Thread Start Rule):Thread對象的start()方法先行發生于此線程的沒一個動作
- 線程中止規則(Thread Termination Rule):Thread對象的中止檢測(如:Thread.join(),Thread.isAlive()等)操作,必行晚于線程中所有操作
- 線程中斷規則(Thread Interruption Rule):對線程的interruption()調用,先于被調用的線程檢測中斷事件(Thread.interrupted())的發生
- 對象中止規則(Finalizer Rule):一個對象的初始化方法先于一個方法執行Finalizer()方法
- 傳遞性(Transitivity):如果操作A先于操作B、操作B先于操作C,則操作A先于操作C
以上就是Happen-Before中的規則。通過這些條件的判定,仍然很難判斷一個線程是否能安全執行,畢竟在我們的時候線程安全多數依賴于工具類的安全性來保證。想提高自己對線程是否安全的判斷能力,必然需要理解所使用的框架或者工具的實現,并積累線程安全的經驗
總結
以上是生活随笔為你收集整理的java内存模型JMM理解整理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql端口establish_sql
- 下一篇: 利用HISTFILESIZE和HISTS