標籤:www 包含 volatile public 監視 共用變數 oid += http
一 引言
在說volatile的記憶體語義時,講過這樣一句話:想要理解透volatile特性有一個很好的方法,就是把對volatile變數的單個讀/寫,看成是使用同一個鎖對這些單個讀/寫操作做了同步。所以其實鎖的釋放和擷取與volatile的寫和讀具有相同的記憶體語義。
二 鎖的釋放-擷取建立的happens-before關係
不清楚happens-before規則的請前去看-看,這裡就不在細說了。由於在之前沒有舉例說明監視器鎖規則,那麼在這裡就詳細說明下,下面是鎖釋放-擷取的範例程式碼:
public class MonitorExample { int a = 0; public synchronized void writer() { // 1 a += 1; // 2 } // 3 public synchronized void reader() { // 4 System.out.println(a); // 5 } // 6 public static void main(String[] args) { final MonitorExample me = new MonitorExample(); Thread t1 = new Thread(new Runnable() { @Override public void run() { me.writer(); } }); t1.start(); Thread t2 = new Thread(new Runnable() { @Override public void run() { me.reader(); } }); t2.start(); }}
這裡我們假設線程1先執行writer()方法,隨後線程B執行reader()方法(知道為什麼要假設?因為不是一定按這種順序發生,可以測試下結果)。根據happens-before規則,這個過程包含的happens-before關係可以分為3類:
1) 根據程式次序規則:1 happens-before 2, 2 happens-before 3, 4 happens-before 5, 5 happens-before 6;
2)根據監視器鎖規則:3 happens-before 4;
3)根據傳遞性規則,2 happens-before 5。
上述happens-before關係的圖形化表現形式如下:
三 鎖的釋放和擷取的記憶體語義
當線程釋放鎖時,JMM會把該線程對應的本地記憶體中的共用變數重新整理到主記憶體中。以上面的MonitorExample程式為例,線程1釋放鎖後,共用資料的狀態如下:
當線程擷取鎖時,JMM會把該線程對應的本地記憶體置為無效。從而使得被監視器保護的臨界區代碼必須從主記憶體中讀取共用變數。
對比鎖釋放-讀取的記憶體語義與volatile寫-讀的記憶體語義可以看出,鎖釋放與volatile寫具有相同的記憶體語義;鎖擷取與volatile讀具有相同的記憶體語義。下面對鎖釋放和鎖擷取的記憶體語義做個總結。
- 線程1釋放一個鎖,實質上是線程1向接下來將要擷取這個鎖的某個線程發出了(線程1對共用變數所做修改的)訊息。
- 線程2擷取一個鎖,實質上是線程2接收了之前某個線程發出的(在釋放這個鎖之前對共用變數所做修改的)訊息。
- 線程1鎖釋放,隨後線程2擷取這個鎖,這個過程實質上是線程A通過主記憶體向線程B發送訊息。
四 鎖記憶體語義的實現
鎖有很多種,但其基本原理都是差不多的。書上是以ReentrantLock中的公平鎖與非公平鎖作為案例分析,有興趣的同學可以去閱讀原籍和源碼。現總結如下:
- 公平鎖和非公平鎖釋放時,最後都要寫一個volatile變數state。
- 公平鎖擷取是,首先會去讀volatile變數。
- 非公平鎖擷取時,首先會用CAS更新volatile變數,這個操作同時具有volatile讀和寫的記憶體語義。
所以鎖釋放-擷取的記憶體語義的實現至少有下面兩種方式:
1)利用volatile變數的寫-讀所具有的記憶體語義。
2)利用CAS所附帶的volatile讀和volatile寫的記憶體語義。
由此可知:並發包下的類的實現方式大部分都是基於這兩種方式實現的。
Java記憶體模型-鎖的記憶體語義