1. 并發(fā)編程基礎概念
并發(fā)——在操作系統(tǒng)中,是指一個時間段中有幾個程序都處于已啟動運行到運行完畢之間宝恶,且這幾個程序都是在同一個處理機上運行溉躲,但任一個時刻點上只有一個程序在處理機上運行——源自百度百科
在并發(fā)編程中孕惜,我們需要處理兩個關鍵問題:線程之間如何通信和線程之間如何同步溶握,后續(xù)篇章將圍繞這兩個問題進行介紹驶睦。
- 線程通信:是指線程之間以何種機制來交換信息砰左,在命令式編程中,線程之間的通信機制有兩種:共享內存和消息傳遞场航。
- 線程同步:是指程序用于控制不同線程之間操作發(fā)生相對順序的機制缠导。在Java中,可以通過volatile溉痢,synchronized, 鎖等方式實現同步僻造。
本文主要介紹java的通信機制,剛介紹常見通信機制主要包括以下兩種方式:
- 共享內存:線程之間共享程序的公共狀態(tài)孩饼,線程之間通過寫-讀內存中的公共狀態(tài)來隱式進行通信髓削。
- 消息傳遞:線程之間沒有公共狀態(tài),線程之間必須通過明確的發(fā)送消息來顯式進行通信镀娶。
Java的并發(fā)采用的是共享內存模型立膛,Java線程之間的通信總是隱式進行,整個通信過程對程序員完全透明汽畴。在java中旧巾,所有實例域耸序、靜態(tài)域和數組元素存儲在堆內存中,堆內存在線程之間共享鲁猩。
Java線程之間的通信由Java內存模型(本文簡稱為JMM)控制坎怪,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。
2. JMM內存模型
JMM(Java Memory Model)是JVM規(guī)范中定義的一種Java內存模型廓握,它的目的是屏蔽掉各種硬件和操作系統(tǒng)的內存訪問差異搅窿,以實現讓Java程序在各種平臺上到能達到一致的內存訪問效果。
Java內存模型的主要定義程序中各個變量的訪問規(guī)則隙券,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣底層細節(jié)男应。首先簡單說明幾個常用名稱定義:
- 變量:這里指包括了實例字段、靜態(tài)字段和構成數組對象的元素娱仔,但是不包括局部變量與方法參數沐飘,后者是線程私有的,不會被共享牲迫。
- 主內存:在java中耐朴,實例域、靜態(tài)域和數組元素是線程之間共享的數據盹憎,它們存儲在主內存中筛峭。
- 工作內存:每條線程都有自己的工作內存,線程的工作內存中保存了該線程使用到的變量到主內存副本拷貝陪每,線程對變量的所有操作(讀取影晓、賦值)都必須在工作內存中進行,而不能直接讀寫主內存中的變量檩禾。
線程、主內存和工作內存的交互關系如上圖所示锌订,和CPU-緩存-內存很類似竹握。
不同線程之間無法直接訪問對方工作內存中的變量画株,線程間變量值的傳遞均需要在主內存來完成辆飘。
最后注意,為了獲得較好的執(zhí)行性能谓传,Java內存模型并沒有限制執(zhí)行引擎使用處理器的寄存器或者高速緩存來提升指令執(zhí)行速度蜈项,也沒有限制編譯器對指令進行重排序。也就是說续挟,在java內存模型中紧卒,也會存在緩存一致性問題和指令重排序的問題。
3. 內存間交互操作
關于主內存與工作內存之間的具體交互協(xié)議诗祸,即一個變量如何從主內存拷貝到工作內存跑芳、如何從工作內存同步到主內存之間的實現細節(jié)轴总,Java內存模型定義了以下八種操作來完成:
- lock(鎖定):作用于主內存的變量,把一個變量標識為一條線程獨占狀態(tài)博个。
- unlock(解鎖):作用于主內存變量怀樟,把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定盆佣。
- read(讀韧ぁ):作用于主內存變量,把一個變量值從主內存?zhèn)鬏數骄€程的工作內存中共耍,以便隨后的load動作使用
- load(載入):作用于工作內存的變量虑灰,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。
- use(使用):作用于工作內存的變量痹兜,把工作內存中的一個變量值傳遞給執(zhí)行引擎穆咐,每當虛擬機遇到一個需要使用變量的值的字節(jié)碼指令時將會執(zhí)行這個操作。
- assign(賦值):作用于工作內存的變量字旭,它把一個從執(zhí)行引擎接收到的值賦值給工作內存的變量庸娱,每當虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作。
- store(存儲):作用于工作內存的變量谐算,把工作內存中的一個變量的值傳送到主內存中熟尉,以便隨后的write的操作。
- write(寫入):作用于主內存的變量洲脂,它把store操作從工作內存中一個變量的值傳送到主內存的變量中斤儿。
所以變量讀寫包含以下幾個步驟:
- 變量從主內存復制到工作內存——順序執(zhí)行read和load操作
- 變量從工作內存同步到主內存——順序執(zhí)行store和write操作
注意,Java內存模型只要求上述操作必須按順序執(zhí)行恐锦,而沒有保證必須是連續(xù)執(zhí)行往果。也就是read和load之間,store和write之間是可以插入其他指令的一铅。
除了定義以上8中原子操作陕贮,Java內存模型還規(guī)定了上述8種基本操作在執(zhí)行時必須滿足一定的操作規(guī)則,例如如不允許read和load單獨出現(即不允許一個變量從主內存中讀取但工作內存不接受)潘飘,不允許store和write單獨出現(即不允許從工作內存中發(fā)起了回寫單主內存不接受)肮之,這里不一一列舉,詳細網上搜索即可卜录。
Java內存模型還定義了volatile型變量的特殊規(guī)則(下一節(jié)介紹)戈擒,以上三種規(guī)定共同確定了Java中哪些內存訪問操作是安全的即:
8種原子操作+操作規(guī)則+volatile規(guī)定=Java中哪些內存訪問操作是安全的
4. volatile型變量規(guī)定
當一個變量被定義為volatile后,將具備兩種特性:
- 特性一:保證此變量對所有線程的可見性
- 特性二:禁止指令重排序優(yōu)化
4.1 volatile可見性
可見性是指當多個線程訪問同一個變量時艰毒,一個線程修改了這個變量的值筐高,其他線程能夠立即看得到修改的值。
普通的共享變量不能保證可見性,因為普通共享變量被修改之后柑土,什么時候被寫入主存是不確定的蜀肘,當其他線程去讀取時,此時內存中可能還是原來的舊值稽屏,因此無法保證可見性幌缝。
但是,需要注意的是volatile變量只保證可見性诫欠,但是java里面的運算并非全部都是原子操作例如++操作涵卵,這樣同樣導致volatile修飾變量java運算不安全。
一般不符合以下兩條規(guī)則的運算場景中荒叼,我們需要通過加鎖(synchronized或并發(fā)包中的鎖)保證變量原子性:
- 運算結果并不依賴變量的當前值轿偎,或者能夠確保只有單一的線程修改變量的值(比如++操作不符合依賴當前值)
- 變量不需要與其他狀態(tài)變量共同參與不變約束
常見的volatile修飾變量的場景是用來作為開關控制并發(fā):
4.2 禁止指令重排序
重排序:是指“編譯器和處理器”為了提高性能,而在程序執(zhí)行時會對程序進行的重排序被廓。大致可以分為以下三類:
- 編譯器優(yōu)化指令重排坏晦,不改變單線程語義的情況下,重新安排指令執(zhí)行的順序嫁乘。
- 指令級并行重排序昆婿,該優(yōu)化主要是為了讓程序發(fā)揮現代處理器的指令級并行執(zhí)行能力,前提是這些語句不存在數據依賴蜓斧。
-
內存系統(tǒng)重排序仓蛆,主要發(fā)生在處理器讀寫緩沖區(qū),讀寫過程看起來是無序的挎春,但最終結果是有序的
從Java源代碼到最終實際執(zhí)行的指令序列看疙,會經過下面三種重排序:
以上重排序可能會導致多線程中出現內存可見性問題,針對編譯器重排序JMM的編譯器重排序規(guī)則會禁止特定類型的編譯器重排序直奋。
而對于后兩種重排序能庆,JMM的處理器重排序規(guī)則會要求java編譯器在生成指令序列時,插入特定類型的內存屏障(memory barriers脚线,intel稱之為memory fence)指令搁胆,通過內存屏障指令來禁止特定類型的處理器重排序(不是所有的處理器重排序都要禁止)。
下面我們看下jvm如何實現volatile禁止指令重排序的:
- volatile變量寫操作邮绿,jvm會向處理器發(fā)送一條Lock前綴命令渠旁,將變量所在的緩存行系會到系統(tǒng)內存。其他處理器通過嗅探總線上傳播的數據檢測自己的數據是否過期斯碌,如果發(fā)現過期會置為無效一死,再次使用時會從系統(tǒng)內存獲取
- Lock前綴命令禁止該指令與之前和之后的讀和寫指令重排序。
最后傻唾,關于volatile禁止重排序幾點使用說明:
- 不會對volatile讀與volatile讀后面的任意內存操作重排序
- 不會對volatile寫與volatile寫之前的任意內存操作重排序
- CAS同時具有volatile讀和寫內存的語義,java的CAS使用現代處理器提供的高效級別的原子指令,這些原子指令以原子方式對內存執(zhí)行讀-改-寫操作冠骄,這是多處理器中實現同步的關鍵伪煤。
5. JMM內存模型總結
總的來說JMM內存模型是圍繞著在并發(fā)過程中如何處理原子性、可見性和有序性三個特征來建立的凛辣。下面就三個特征分別說明:
5.1 原子性
原子性:即一個操作或者多個操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷抱既,要么就都不執(zhí)行。
java內存模型的read扁誓、load防泵、assign、use蝗敢、store和write六個操作直接保證原子性捷泞,我們可以任務基本數據類型訪問讀寫是具有原子性(特殊說明long double64位操作根據jvm實現有關)。
如果場景中需要大范圍的原子性保證寿谴,java內存模型提供了lock和unlock操作來滿足锁右,對應到java代碼關鍵字即是——synchronized。
5.2 可見性
可見性是指當多個線程訪問同一個變量時讶泰,一個線程修改了這個變量的值咏瑟,其他線程能夠立即看得到修改的值。
除了上面介紹的volatile外痪署,java還提供了兩個關鍵字實現可見性码泞,synchronized和final。
- final的可見性:是指被final修飾的字段在構造器中一旦完成狼犯,那么在其他線程就可以看見final字段值
- synchronized可見性:是指對一個變量執(zhí)行unlock操作之前浦夷,必須先把次變量同步會主內存這條操作規(guī)則限制
5.3 有序性
有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
java中天然有序性可以總結為一句話:如果在本線程內觀察辜王,所有的操作都是有序的劈狐;如果在一個線程中觀察另外一個線程,所有操作都是無序的呐馆。前半句是指“線程內表現為串行語義”肥缔,后半句表示“指令重排”和“工作內存與主內存同步延遲”現象。
java提供了volatile和synchronized兩個關鍵字來保證線程之間操作的有序性汹来,這里synchronized則是有“同一時刻只允許一條線程對其進行l(wèi)ock操作”這條操作規(guī)定獲取的续膳,這個規(guī)則決定了同一個鎖的兩個同步塊只能串行進入。
最后收班,可以發(fā)現synchronized關鍵字可以同時解決上述三個問題坟岔,當然這個需要付出代價就是性能問題。
參考文檔
《深入理解java虛擬機》——周志明
http://www.cnblogs.com/dolphin0520/p/3920373.html