java 記憶體模型-07-java 鎖 ReentrantLock
鎖的釋放和獲取
鎖是 java 併發程式設計中最重要的同步機制。
鎖除了讓臨界區互斥執行 外,還可以讓釋放鎖的執行緒向獲取同一個鎖的執行緒傳送訊息。
例項
- MonitorExample.java
class MonitorExample { int a = 0; public synchronized void writer() {//1 a++;//2 }//3 public synchronized void reader() {//4 int i = a;//5 //…… }//6 }
假設執行緒 A 執行writer()
方法,隨後執行緒 B 執行reader()
方法。
根據happens-before
規則,這個過程包含的 happens-before 關係可以分為兩類:
-
根據程式次序規則,1 happens before 2, 2 happens before 3; 4 happens before 5, 5 happens before 6。
-
根據監視器鎖規則,3 happens before 4。
-
根據 happens before 的傳遞性,2 happens before 5。
因此,執行緒 A 在釋放鎖之前所有可見的共享變數,線上程 B 獲取同一個鎖之後,將立刻變得對 B 執行緒可見。
鎖釋放和獲取的記憶體語義
當執行緒釋放鎖時,JMM 會把該執行緒對應的本地記憶體中的共享變數重新整理到主記憶體中。
以上面的 MonitorExample 程式為例,A 執行緒釋放鎖後,共享資料的狀態示意圖如下:
- 執行緒 A
本地記憶體 A: a = 1; (寫入到主記憶體) 主記憶體:a = 1;
當執行緒獲取鎖時,JMM 會把該執行緒對應的本地記憶體置為無效。
從而使得被監視器保護的臨界區程式碼必須要從主記憶體中去讀取共享變數。
下面是鎖獲取的狀態過程:
線上程 A 寫入主記憶體之後。
執行緒之間通訊:執行緒 A 向 B 傳送訊息
- 執行緒 B
主記憶體:a = 1; (從主記憶體中讀取) 本地記憶體 B: a = 1;
和 volatile 記憶體語義對比
對比鎖釋放-獲取的記憶體語義與 volatile 寫-讀的記憶體語義,
可以看出:鎖釋放與 volatile 寫有相同的記憶體語義;鎖獲取與 volatile 讀有相同的記憶體語義 。
總結
下面對鎖釋放和鎖獲取的記憶體語義做個總結:
-
執行緒 A 釋放一個鎖,實質上是執行緒A向接下來將要獲取這個鎖的某個執行緒發出了(執行緒A對共享變數所做修改的)訊息。
-
執行緒 B 獲取一個鎖,實質上是執行緒 B 接收了之前某個執行緒發出的(在釋放這個鎖之前對共享變數所做修改的)訊息。
-
執行緒 A 釋放鎖,隨後執行緒 B 獲取這個鎖,這個過程實質上是執行緒 A 通過主記憶體向執行緒 B 傳送訊息。
鎖記憶體語義的實現
本文將藉助ReentrantLock
的原始碼,來分析鎖記憶體語義的具體實現機制。
- ReentrantLockExample.java
class ReentrantLockExample { int a = 0; ReentrantLock lock = new ReentrantLock(); public void writer() { lock.lock();//獲取鎖 try { a++; } finally { lock.unlock();//釋放鎖 } } public void reader () { lock.lock();//獲取鎖 try { int i = a; //…… } finally { lock.unlock();//釋放鎖 } } }
在 ReentrantLock 中,呼叫 lock() 方法獲取鎖;呼叫 unlock() 方法釋放鎖。
原始碼實現
ReentrantLock 的實現依賴於 java 同步器框架AbstractQueuedSynchronizer
(本文簡稱之為AQS)。
AQS 使用一個整型的volatile
變數(命名為state)來維護同步狀態,馬上我們會看到,這個volatile
變數是 ReentrantLock 記憶體語義實現的關鍵。
public class ReentrantLock implements Lock, java.io.Serializable { /** * Base of synchronization control for this lock. Subclassed * into fair and nonfair versions below. Uses AQS state to * represent the number of holds on the lock. */ abstract static class Sync extends AbstractQueuedSynchronizer { //... } /** * Sync object for non-fair locks */ static final class NonfairSync extends Sync { //... } /** * Sync object for fair locks */ static final class FairSync extends Sync { //... } }
公平鎖
lock()
使用公平鎖時,加鎖方法lock()的方法呼叫軌跡如下:
-
ReentrantLock : lock()
-
FairSync : lock()
-
AbstractQueuedSynchronizer : acquire(int arg)
-
ReentrantLock : tryAcquire(int acquires)
在第 4 步真正開始加鎖,下面是該方法的原始碼(JDK 1.8):
/** * Fair version of tryAcquire.Don't grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
加鎖方法首先讀 volatile 變數 state。
unlock()
在使用公平鎖時,解鎖方法unlock()的方法呼叫軌跡如下:
-
ReentrantLock : unlock()
-
AbstractQueuedSynchronizer : release(int arg)
-
Sync : tryRelease(int releases)
在第 3 步真正開始釋放鎖,下面是該方法的原始碼:
protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
在釋放鎖的最後寫 volatile 變數 state。
公平鎖在釋放鎖的最後寫 volatile 變數 state;在獲取鎖時首先讀這個 volatile 變數。
根據 volatile 的 happens-before 規則,釋放鎖的執行緒在寫 volatile 變數之前可見的共享變數,在獲取鎖的執行緒讀取同一個 volatile 變數後將立即變的對獲取鎖的執行緒可見。
非公平鎖
非公平鎖的釋放和公平鎖完全一樣,所以這裡僅僅分析非公平鎖的獲取。
lock()
使用非公平鎖時,加鎖方法lock()的方法呼叫軌跡如下:
-
ReentrantLock : lock()
-
NonfairSync : lock()
-
AbstractQueuedSynchronizer : compareAndSetState(int expect, int update)
在第 3 步真正開始加鎖,下面是該方法的原始碼:
/** * Atomically sets synchronization state to the given updated * value if the current state value equals the expected value. * This operation has memory semantics of a {@code volatile} read * and write. * * @param expect the expected value * @param update the new value * @return {@code true} if successful. False return indicates that the actual *value was not equal to the expected value. */ protected final boolean compareAndSetState(int expect, int update) { // See below for intrinsics setup to support this return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
該方法以原子操作的方式更新 state 變數,本文把 java 的 compareAndSet() 方法呼叫簡稱為CAS。
JDK文件對該方法的說明如下:如果當前狀態值等於預期值,則以原子方式將同步狀態設定為給定的更新值。此操作具有 volatile 讀和寫的記憶體語義 。
總結
現在對公平鎖和非公平鎖的記憶體語義做個總結:
-
公平鎖和非公平鎖釋放時,最後都要寫一個 volatile 變數 state。
-
公平鎖獲取時,首先會去讀這個 volatile 變數。
-
非公平鎖獲取時,首先會用 CAS 更新這個 volatile 變數,這個操作同時具有 volatile 讀和 volatile 寫的記憶體語義。
從本文對 ReentrantLock 的分析可以看出,鎖釋放-獲取的記憶體語義的實現至少有下面兩種方式:
-
利用 volatile 變數的寫-讀所具有的記憶體語義。
-
利用 CAS 所附帶的 volatile 讀和 volatile 寫的記憶體語義。
concurrent 包
由於 java 的 CAS 同時具有 volatile 讀和 volatile 寫的記憶體語義,因此 java 執行緒之間的通訊現在有了下面四種方式:
-
A 執行緒寫 volatile 變數,隨後B執行緒讀這個 volatile 變數。
-
A 執行緒寫 volatile 變數,隨後 B 執行緒用 CAS 更新這個 volatile 變數。
-
A 執行緒用 CAS 更新一個 volatile 變數,隨後 B 執行緒用 CAS 更新這個 volatile 變數。
-
A 執行緒用 CAS 更新一個 volatile 變數,隨後B執行緒讀這個 volatile 變數。
ps: 簡而言之,volatile 和 CAS 的讀寫組合。
Java 的 CAS 會使用現代處理器上提供的高效機器級別原子指令,這些原子指令以原子方式對記憶體執行讀-改-寫操作,這是在多處理器中實現同步的關鍵(從本質上來說,能夠支援原子性讀-改-寫指令的計算機器,是順序計算圖靈機的非同步等價機器,因此任何現代的多處理器都會去支援某種能對記憶體執行原子性讀-改-寫操作的原子指令)。
同時,volatile 變數的讀/寫和CAS可以實現執行緒之間的通訊。
把這些特性整合在一起,就形成了整個 concurrent 包得以實現的基石。
通用模式
如果我們仔細分析 concurrent 包的原始碼實現,會發現一個通用化的實現模式:
首先,宣告共享變數為 volatile;
然後,使用 CAS 的原子條件更新來實現執行緒之間的同步;
同時,配合以 volatile 的讀/寫和CAS所具有的 volatile 讀和寫的記憶體語義來實現執行緒之間的通訊。
AQS
AQS,非阻塞資料結構和原子變數類(java.util.concurrent.atomic
包中的類)
這些 concurrent 包中的基礎類都是使用這種模式來實現的,而 concurrent 包中的高層類又是依賴於這些基礎類來實現的。
參考資料
-
JSR 133
-
other
ofollow,noindex">http://www.infoq.com/cn/articles/java-memory-model-5