讀寫鎖ReentrantReadWriteLock
讀寫鎖維護一對鎖,一個讀鎖和一個寫鎖。讀寫鎖在同一時刻可以允許多個讀執行緒訪問,但是在寫執行緒訪問時,所有的讀執行緒和其他寫執行緒均被阻塞。一般情況下,讀寫鎖的效能都會比排它鎖好,因為大多數場景讀是多於寫的。特性如下:
- 公平性選擇
- 重進入
- 鎖降級
1.讀寫鎖的介面與例項
ReadWriteLock僅定義了獲取讀鎖和寫鎖的兩個方法,即readLock()方法和writeLock()方法,而其實現——ReentrantReadWriteLock除了介面方法之外,還提供了一些便於外界監控其內部工作狀態的方法。
通過一個快取示例說明讀寫鎖的使用方式
public class Cache { static Map<String, Object> map = new HashMap<String, Object>(); static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); static Lock r = rwl.readLock(); static Lock w = rwl.writeLock(); // 獲取一個key對應的value public static final Object get(String key) { r.lock(); try { return map.get(key); } finally { r.unlock(); } } // 設定key對應的value,並返回舊的value public static final Object put(String key, Object value) { w.lock(); try { return map.put(key, value); } finally { w.unlock(); } } // 清空所有的內容 public static final void clear() { w.lock(); try { map.clear(); } finally { w.unlock(); } } }
2.讀寫鎖的實現
主要包括:讀寫狀態的設計、寫鎖的獲取與釋放、讀鎖的獲取與釋放以及鎖降級。
2.1讀寫狀態的設計
讀寫鎖同樣依賴自定義同步器來實現同步功能,,而讀寫狀態就是其同步器的同步狀態(state變數)。
一個整型變數上維護多種狀態,就一定需要“按位切割使用”這個變數,讀寫鎖將變數切分成了兩個部分,高16位表示讀,低16位表示寫。
當前同步狀態表示一個執行緒已經獲取了寫鎖,且重進入了兩次,同時也連續獲取了兩次讀鎖。
假設當前同步狀態值為S,寫狀態等於S&0x0000FFFF(將高16位全部抹去),讀狀態等於S>>>16(無符號補0右移16位)。當寫狀態增加1時,等於S+1,當讀狀態增加1時,等於S+(1<<16),也就是S+0x00010000。
根據狀態的劃分能得出一個推論:S不等於0時,當寫狀態(S&0x0000FFFF)等於0時,則讀狀態(S>>>16)大於0,即讀鎖已被獲取。
2.2寫鎖的獲取與釋放
寫鎖是一個支援重進入的排它鎖,如果當前執行緒在獲取寫鎖時,讀鎖已經被獲取(讀狀態不為0)或者該執行緒不是已經獲取寫鎖的執行緒,則當前執行緒進入等待狀態。
ReentrantReadWriteLock的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// 存在讀鎖或者當前獲取執行緒不是已經獲取寫鎖的執行緒
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
setState(c + acquires);
return true;
}
if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) {
return false;
}
setExclusiveOwnerThread(current);
return true;
}
如果存在讀鎖,則寫鎖不能被獲取,原因在於:讀寫鎖要確保寫鎖的操作對讀鎖可見。
寫鎖的釋放與ReentrantLock的釋放過程基本類似,每次釋放均減少寫狀態,當寫狀態為0時表示寫鎖已被釋放,從而等待的讀寫執行緒能夠繼續訪問讀寫鎖,同時前次寫執行緒的修改對後續讀寫執行緒可見。
2.3讀鎖的獲取釋放
protected final int tryAcquireShared(int unused) {
for (;;) {
int c = getState();
int nextc = c + (1 << 16);
if (nextc < c)
throw new Error("Maximum lock count exceeded");
if (exclusiveCount(c) != 0 && owner != Thread.currentThread())
return -1;
if (compareAndSetState(c, nextc))
return 1;
}
}
如果其他執行緒已經獲取了寫鎖,則當前執行緒獲取讀鎖失敗,進入等待狀態。如果當前執行緒獲取了寫鎖或者寫鎖未被獲取,則當前執行緒(執行緒安全,
依靠CAS保證)增加讀狀態,成功獲取讀鎖。
讀鎖的每次釋放(執行緒安全的,可能有多個讀執行緒同時釋放讀鎖)均減少讀狀態,減少的值是(1<<16)。
2.4鎖降級
鎖降級指的是寫鎖降級成為讀鎖。鎖降級是指把持住(當前擁有的)寫鎖,再獲取到讀鎖,隨後釋放(先前擁有的)寫鎖的過程。
public void processData() {
readLock.lock();
if (!update) {
// 必須先釋放讀鎖
readLock.unlock();
// 鎖降級從寫鎖獲取到開始
writeLock.lock();
try {
if (!update) {
// 準備資料的流程(略)
update = true;
}
readLock.lock();
} finally {
writeLock.unlock();
}
// 鎖降級完成,寫鎖降級為讀鎖
}
try {
// 使用資料的流程(略)
} finally {
readLock.unlock();
}
}
鎖降級中讀鎖的獲取是否必要呢?答案是必要的。主要是為了保證資料的可見性,如果當前執行緒不獲取讀鎖而是直接釋放寫鎖,假設此刻另一個執行緒(記作執行緒T)獲取了寫鎖並修改了資料,那麼當前執行緒無法感知執行緒T的資料更新。如果當前執行緒獲取讀鎖,即遵循鎖降級的步驟,則執行緒T將會被阻塞,直到當前執行緒使用資料並釋放讀鎖之後,執行緒T才能獲取寫鎖進行資料更新。
RentrantReadWriteLock不支援鎖升級, 鎖升級無法保