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