JMM中的原子性、可见性、有序性和volatile关键字
相信如果對JMM底層有過了解或者接觸過java并發(fā)編程的讀者對以上的概念并不陌生,但是真正理解的可能并不多。這里我就對這些概念再做一次講解。相信讀者多讀幾遍應該就有自己的理解,實在不理解也沒關(guān)系,說明知識的儲備還不夠,不妨以后再來讀一遍,可能會瞬間突然明白。
參考內(nèi)容:
JMM的關(guān)鍵技術(shù)點都是圍繞著多線程的原子性、可見性和有序性來建立的。要理解JMM首先就需要了解這三個特性。而volatile關(guān)鍵字很好的貫徹了可見性和有序性,提到volatile關(guān)鍵字也是為了加深對這三個特性的理解,同時volatile也是非常重要的內(nèi)容,也是難點。
1、原子性
原子性其實非常好理解,原子性操作就是指這些操作是不可中斷的,要做一定做完,要么就沒有執(zhí)行,也就是不可被中斷。
我們使用的int類型的數(shù)據(jù)如果只是是簡單的讀取和賦值的話就是原子操作。下面給出幾個例子:
i = 2; 賦值給i -----------------------------操作步驟:1 原子操作 j = i; 讀取i值 賦值給j -----------------------操作步驟:2 非原子操作 i++; 讀取i值 i值加1 賦值給i ------------------操作步驟:3 非原子操作 i = i+1; 讀取i值 i值加1 賦值給i --------------操作步驟:3 非原子操作但是如果我們不使用int型而使用long型的話,對于32位系統(tǒng)來說,long型數(shù)據(jù)的讀寫不是原子性的(因為long有64位)。虛擬機規(guī)范中允許對 64位數(shù)據(jù)類型( long和 double),分為 2次 32為的操作來處理,也就是說,如果兩個線程同時對long進行寫入的話(或者讀取),對線程之間的結(jié)果是有干擾的。可能高位是一個線程寫的,低位又是另一個線程寫的,如果這時候讀的話,就會讀到錯誤的值。不是線程1寫的值,也不是線程2寫的值。但是最新 JDK實現(xiàn)還是實現(xiàn)了原子操作的。JMM只實現(xiàn)了基本的原子性,像上面 i++那樣的操作,必須借助于 synchronized和 Lock來保證整塊代碼的原子性了。
2、可見性
可見性是指當一個線程修改了某一個共享變量的值,其他線程是否能夠立即知道這個修改。
顯然,對于串行程序來說,可見性問題是不存在的。因為你在任何一個操作步驟中修改了某個變量,那么在后續(xù)的步驟中,讀取這個變量的值,一定是修改后的新值。但是這個問題在并行程序中就不見得了。如果一個線程修改了某一個全局變量,那么其他線程未必可以馬上知道這個改動。這個問題可能由cache優(yōu)化引起,比如下面這個例子:
如果在CPU1和CPU2上各運行了一個線程,它們共享變量t,由于編譯器優(yōu)化或者硬件優(yōu)化的緣故,在CPU1上的線程將變量t進行了優(yōu)化,將其緩存在cache中或者寄存器里。這種情況下,如果在CPU2上的某個線程修改了變量t的實際值,那么CPU1上的線程可能并無法意識到這個改動,依然會讀取cache中或者寄存器里的數(shù)據(jù)。因此,就產(chǎn)生了可見性問題。外在表現(xiàn)為:變量t的值被修改,但是CPU1上的線程依然會讀到一個舊值。可見性問題也是并行程序開發(fā)中需要重點關(guān)注的問題之一。
可見性問題是一個綜合性問題。除了上述提到的緩存優(yōu)化或者硬件優(yōu)化(有些內(nèi)存讀寫可能不會立即觸發(fā),而會先進入一個硬件隊列等待)會導致可見性問題外,指令重排(這個問題將在下一節(jié)中更詳細討論)以及編輯器的優(yōu)化,都有可能導致一個線程的修改不會立即被其他線程察覺。
3、有序性
JMM是允許編譯器和處理器對指令重排序的,但是規(guī)定了 as-if-serial語義,即不管怎么重排序,程序的執(zhí)行結(jié)果不能改變。比如下面的程序段:
double pi = 3.14; // A double r = 1; // B doubles = pi *r *r; // C無論是 A->B->C 還是 B->A->C 都對結(jié)果沒有影響。但是這是發(fā)生在單線程之中的。在多線程之中可能就不是這樣了,多線程有序性引起的問題我們可以看一個典型的例子:
class OrderExample{int a = 0;boolean flag = false;public void writer(){a = 1;flag = true;}public void reader(){if(flag){int i = a + 1;}} }如果這個類的writer()和reader()方法是在不同的線程中運行的。那么writer()中的方法可能會被重排序為flag= true先執(zhí)行。這個時候如果被中斷,換到執(zhí)行reader()的線程執(zhí)行,flag為true,進入if判斷就會自然認為a = 1;但是這個時候a還是0。這里大概就能理解重排序帶來的問題了。
JMM具備一些先天的有序性,即不需要通過任何手段就可以保證的有序性,也就是在下面這些情況中,是不能進行重排序的。通常稱為 happens-before原則
關(guān)于為什么需要重排序,這里再詳細說明一下:
比如執(zhí)行:
在cpu中執(zhí)行的過程可能是這樣:
左邊是匯編指令,右邊就是流水線的情況。注意,在ADD指令上,有一個大叉,表示一個中斷。也就是說ADD在這里停頓了一下。為什么ADD會在這里停頓呢?原因很簡單,R2中的數(shù)據(jù)還沒有準備好!所以,ADD操作必須進行一次等待。由于ADD的延遲,導致其后面所有的指令都要慢一個節(jié)拍。
既然停頓是因為數(shù)據(jù)還沒有準備好,那我們就在它等待數(shù)據(jù)準備好的時候做其他事情。也就是在ADD和前面的LW指令之間插入一個做其他事情的指令,SUB同理,具體來說我們可以這樣移動指令:
變成:
可以看到一共節(jié)約了兩步執(zhí)行時間。
4、volatile關(guān)鍵字
被 volatile修飾的共享變量,具有以下兩點特性:
JMM規(guī)定對一個 volatile域的寫, happens-before于后續(xù)對這個 volatile域的讀(也就是一個線程寫了volatile域,其他線程如果執(zhí)行讀操作就會知道它改變了),其實就是如果一個變量聲明成是 volatile的,那么當我讀變量時,總是能讀到它的最新值,這里最新值是指不管其它哪個線程對該變量做了寫操作,都會立刻被更新到主存里,我也能從主存里讀到這個剛寫入的值。
從內(nèi)存語義上來看
當寫一個volatile變量時,JMM會把該線程對應的本地內(nèi)存中的共享變量刷新到主內(nèi)存
當讀一個volatile變量時,JMM會把該線程對應的本地內(nèi)存置為無效,線程接下來將從主內(nèi)存中讀取共享變量。
關(guān)于禁止重排序也就是不會讓volatile寫之前執(zhí)行的結(jié)果跑到后面去,再拿這個例子說明就是a=1;不會到flag = true之后執(zhí)行。
class OrderExample{int a = 0;boolean flag = false;public void writer(){a = 1;flag = true;}public void reader(){if(flag){int i = a + 1;}} }同時保證volatile讀之后的操作不會到volatile讀之前操作;
但是volatile是無法保證原子性的,但是和int類型變量一樣,簡單的讀取賦值還是原子的,但是這和volatile關(guān)鍵字沒什么關(guān)系,只是作為普通變量的特性。舉個例子,比如線程A讀取了volatile變量,阻塞了。換到線程B讀取了volatile變量執(zhí)行加1操作,寫回。現(xiàn)在又切換到線程A,因為線程A已經(jīng)執(zhí)行了讀操作,無法觸發(fā)線程A感知volatile變量已經(jīng)改變,只有在做讀取操作時,發(fā)現(xiàn)自己緩存行無效,才會去讀主存的值,所以該線程直接加1,寫回。所以雖然執(zhí)行了2次加1,但實際只加了一次加1。理解只有volatile變量的讀操作才能觸發(fā)線程感知變量已經(jīng)改變是非常重要的。
關(guān)于volatile的底層實現(xiàn)機制:
如果把加入 volatile關(guān)鍵字的代碼和未加入 volatile關(guān)鍵字的代碼都生成匯編代碼,會發(fā)現(xiàn)加入 volatile關(guān)鍵字的代碼會多出一個 lock前綴指令。
lock前綴指令實際相當于一個內(nèi)存屏障,內(nèi)存屏障提供了以下功能:
1 . 重排序時不能把后面的指令重排序到內(nèi)存屏障之前的位置
2 . 使得本CPU的Cache寫入內(nèi)存
3 . 寫入動作也會引起別的CPU或者別的內(nèi)核無效化其Cache,相當于讓新寫入的值對別的線程可見。
關(guān)于volatile的使用場景
使用volatile來標示flag,就能解決上面說到的可見性問題,這種對變量的讀寫操作,標記為 volatile可以保證修改對線程立刻可見。比 synchronized, Lock有一定的效率提升。
這是一種懶漢的單例模式,使用時才創(chuàng)建對象,為了避免初始化操作的指令重排序,給 instance加上了 volatile。
總結(jié)
以上是生活随笔為你收集整理的JMM中的原子性、可见性、有序性和volatile关键字的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java 7 并发编程指南
- 下一篇: 使用Fork/Join框架优化归并排序