標籤:運行 開始 unlock 大致 int 協助 mina 進入 時間
要想並發程式正確地執行,必須要保證原子性、可見度以及有序性。只要有一個沒有被保證,就有可能會導致程式運行不正確。
1、原子性(Atomicity)
原子性是指在一個操作中就是cpu不可以在中途暫停然後再調度,既不被中斷操作,要不執行完成,要不就不執行。
如果一個操作時原子性的,那麼多線程並發的情況下,就不會出現變數被修改的情況
比如 a=0;(a非long和double類型) 這個操作是不可分割的,那麼我們說這個操作時原子操作。再比如:a++; 這個操作實際是a = a + 1;是可分割的,所以他不是一個原子操作。
非原子操作都會存線上程安全問題,需要我們使用同步技術(sychronized)來讓它變成一個原子操作。一個操作是原子操作,那麼我們稱它具有原子性。java的concurrent包下提供了一些原子類,我們可以通過閱讀API來瞭解這些原子類的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
(由Java記憶體模型來直接保證的原子性變數操作包括read、load、use、assign、store和write六個,大致可以認為基礎資料類型的訪問和讀寫是具備原子性的。如果應用情境需要一個更大範圍的原子性保證,Java記憶體模型還提供了lock和unlock操作來滿足這種需求,儘管虛擬機器未把lock與unlock操作直接開放給使用者使用,但是卻提供了更高層次的位元組碼指令monitorenter和monitorexit來隱匿地使用這兩個操作,這兩個位元組碼指令反映到Java代碼中就是同步塊—synchronized關鍵字,因此在synchronized塊之間的操作也具備原子性。)
2、可見度(Visibility)
可見度就是指當一個線程修改了線程共用變數的值,其它線程能夠立即得知這個修改。Java記憶體模型是通過在變數修改後將新值同步回主記憶體,在變數讀取前從主記憶體重新整理變數值這種依賴主記憶體作為傳遞媒介的方法來實現可見度的,無論是普通變數還是volatile變數都是如此,普通變數與volatile變數的區別是volatile的特殊規則保證了新值能立即同步到主記憶體,以及每使用前立即從記憶體重新整理。因為我們可以說volatile保證了線程操作時變數的可見度,而普通變數則不能保證這一點。
除了volatile之外,Java還有兩個關鍵字能實現可見度,它們是synchronized。同步塊的可見度是由“對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體中(執行store和write操作)”這條規則獲得的,而final關鍵字的可見度是指:被final修飾的欄位是構造器一旦初始化完成,並且構造器沒有把“this”引用傳遞出去,那麼在其它線程中就能看見final欄位的值。
(可見度,是指線程之間的可見度,一個線程修改的狀態對另一個線程是可見的。也就是一個線程修改的結果。另一個線程馬上就能看到。比如:用volatile修飾的變數,就會具有可見度。volatile修飾的變數不允許線程內部緩衝和重排序,即直接修改記憶體。所以對其他線程是可見的。但是這裡需要注意一個問題,volatile只能讓被他修飾內容具有可見度,但不能保證它具有原子性。比如 volatile int a = 0;之後有一個操作 a++;這個變數a具有可見度,但是a++ 依然是一個非原子操作,也就這這個操作同樣存線上程安全問題。)
3、有序性(Ordering)
Java記憶體模型中的程式天然有序性可以總結為一句話:如果在本線程內觀察,所有操作都是有序的;如果在一個線程中觀察另一個線程,所有操作都是無序的。前半句是指“線程內表現為串列語義”,後半句是指“指令重排序”現象和“工作記憶體中主記憶體同步延遲”現象。
Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操作的有序性,volatile關鍵字本身就包含了禁止指令重排序的語義,而synchronized則是由“一個變數在同一時刻只允許一條線程對其進行lock操作”這條規則來獲得的,這個規則決定了持有同一個鎖的兩個同步塊只能串列地進入。
先行發生原則:
如果Java記憶體模型中所有的有序性都只靠volatile和synchronized來完成,那麼有一些操作將會變得很囉嗦,但是我們在編寫Java並發代碼的時候並沒有感覺到這一點,這是因為Java語言中有一個“先行發生”(Happen-Before)的原則。這個原則非常重要,它是判斷資料是否存在競爭,線程是否安全的主要依賴。
先行發生原則是指Java記憶體模型中定義的兩項操作之間的依序關係,如果說操作A先行發生於操作B,其實就是說發生操作B之前,操作A產生的影響能被操作B觀察到,“影響”包含了修改了記憶體中共用變數的值、發送了訊息、調用了方法等。它意味著什麼呢?如下例:
//線程A中執行
i = 1;
//線程B中執行
j = i;
//線程C中執行
i = 2;
假設線程A中的操作”i=1“先行發生於線程B的操作”j=i“,那麼我們就可以確定線上程B的操作執行後,變數j的值一定是等於1,結出這個結論的依據有兩個,一是根據先行發生原則,”i=1“的結果可以被觀察到;二是線程C登場之前,線程A操作結束之後沒有其它線程會修改變數i的值。現在再來考慮線程C,我們依然保持線程A和B之間的先行發生關係,而線程C出現線上程A和B操作之間,但是C與B沒有先行發生關係,那麼j的值可能是1,也可能是2,因為線程C對應變數i的影響可能會被線程B觀察到,也可能觀察不到,這時線程B就存在讀取到到期資料的風險,不具備多線程的安全性。
下面是Java記憶體模型下一些”天然的“先行發生關係,這些先行發生關係無須任何同步器協助就已經存在,可以在編碼中直接使用。如果兩個操作之間的關係不在此列,並且無法從下列規則推匯出來的話,它們就沒有順序性保障,虛擬機器可以對它們進行隨意地重排序。
a.程式次序規則(Pragram Order Rule):在一個線程內,按照程式碼順序,書寫在前面的操作先行發生於書寫在後面的操作。準確地說應該是控制流程順序而不是程式碼順序,因為要考慮分支、迴圈結構。
b.管程鎖定規則(Monitor Lock Rule):一個unlock操作先行發生於後面對同一個鎖的lock操作。這裡必須強調的是同一個鎖,而”後面“是指時間上的先後順序。
c.volatile變數規則(Volatile Variable Rule):對一個volatile變數的寫操作先行發生於後面對這個變數的讀取操作,這裡的”後面“同樣指時間上的先後順序。
d.線程啟動規則(Thread Start Rule):Thread對象的start()方法先行發生於此線程的每一個動作。
e.線程終於規則(Thread Termination Rule):線程中的所有操作都先行發生於對此線程的終止檢測,我們可以通過Thread.join()方法結束,Thread.isAlive()的傳回值等作段檢測到線程已經終止執行。
f.線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測是否有中斷髮生。
g.對象終結規則(Finalizer Rule):一個對象初始化完成(構造方法執行完成)先行發生於它的finalize()方法的開始。
g.傳遞性(Transitivity):如果操作A先行發生於操作B,操作B先行發生於操作C,那就可以得出操作A先行發生於操作C的結論。
一個操作”時間上的先發生“不代表這個操作會是”先行發生“,那如果一個操作”先行發生“是否就能推匯出這個操作必定是”時間上的先發生“呢?也是不成立的,一個典型的例子就是指令重排序。所以時間上的先後順序與先生髮生原則之間基本沒有什麼關係,所以衡量並發安全問題一切必須以先行發生原則為準。
原文地址:
51442192
java並發特性:原子性、可見度、有序性