上一篇裏講的ReentrankLock是一種排他鎖,即同一時間只能有一個線程進入。而讀寫鎖在同一時刻允許多個讀線程訪問,但是在寫線程訪問時,所有的讀線程和其他線程均被阻塞。讀寫鎖維護了一對鎖,一個讀鎖和一個寫鎖,通過分離讀寫鎖,使得併發性比一般的排它鎖有了很大提升。因爲大多數應用場景都是讀多於寫的,因此在這樣的情況下,讀寫鎖可以提高吞吐量。下圖描述了關於讀寫鎖的三個特性:公平性、重入性和鎖降級。
1 讀寫鎖的使用
/**
* 利用讀寫鎖實現的一個數據結構
* @author songxu
*
*/
public class ReadWriteCache
{
//利用hashmap作爲底層數據結構
private Map<String, Object> cache=new HashMap<String, Object>();
//構造讀寫鎖
private ReentrantReadWriteLock readwritelock=new ReentrantReadWriteLock();
//讀鎖
private Lock readLock=readwritelock.readLock();
//寫鎖
private Lock writeLock=readwritelock.writeLock();
/**
* 存入數據
* @param key 鍵
* @param value 值
*/
public void put(String key,Object value)
{
writeLock.lock();
//鎖一定在try塊之外
try {
cache.put(key, value);
}
finally
{
writeLock.unlock();
}
}
/**
* 獲取數據
* @param key 鍵
* @return 值
*/
public Object get(String key)
{
readLock.lock();
try {
return cache.get(key);
}
finally
{
readLock.unlock();
}
}
}
在上述代碼中,put方法在更新或插入數據前必須提前獲取寫鎖,當獲取寫鎖之後,其他線程對於讀鎖和寫鎖的獲取均被阻塞,只有寫鎖釋放後,其他讀操作才能繼續。在get方法中,需要獲取讀鎖,而此時其他線程均可訪問該方法而不被阻塞。可以說這是一個類似於concurrentHashMap的原型,但它的效率肯定沒有concurrentHashMap高,但應該比HashTable要強一些。2 讀寫鎖的實現原理
2.1 讀寫狀態
/*
* Read vs write count extraction constants and functions.
* Lock state is logically divided into two unsigned shorts:
* The lower one representing the exclusive (writer) lock hold count,
* and the upper the shared (reader) hold count.
*/
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
2.2 寫鎖的獲取與釋放
- 讀鎖的獲取實際定義在內部同步器Sync的tryAcquire方法中,其源碼如下:
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
Thread current = Thread.currentThread();
int c = getState();//獲取當前狀態
int w = exclusiveCount(c);//獲取寫的狀態
if (c != 0) {
//如果存在讀鎖 或者當前線程不是獲取寫鎖的線程 返回false
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;
}
該方法的邏輯很簡單,首先判斷讀寫狀態。如果不爲0且存在讀鎖或者已存在的寫鎖並非當前線程獲取到,則寫鎖不能獲取,只能等待其他線程都釋放了鎖,才能獲取。- 讀鎖的釋放實際定義在內部同步器Sync的tryRelease方法中,其源碼如下:
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
這個釋放過程與ReentrantLock的過程基本類似,每次釋放均減少些狀態,當寫狀態爲0時表示寫鎖已經被釋放。2.3 讀鎖的獲取與釋放
讀鎖是一個支持重進入的共享鎖,它能夠被多個線程同時獲取,在沒有其他線程訪問時,讀鎖總會被成功地獲取。如果當前線程已經獲取了讀鎖,則增加讀狀態,如果獲取讀鎖時寫鎖已經被其他線程獲取,則進入等待狀態。
- 讀鎖的獲取定義在內部同步器Sync的tryAcquireShared方法中
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
Thread current = Thread.currentThread();
int c = getState();
//如果其他線程已經獲取了寫鎖,則失敗
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);//獲取讀鎖的數量
if (!readerShouldBlock() &&
r < MAX_COUNT &&
//CAS更新狀態 因爲可能多線程操作
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
這個tryRelease方法首先需要檢查是否其他線程已經獲取了寫鎖,如果被獲取則當前線程失敗,進入等待狀態。否則如果當前線程滿足對state的操作條件,就利用CAS設置state+SHARED_UNIT,實際上就是讀狀態+1。但是需要注意,這個state是全局的,即所有線程獲取讀鎖次數的總和,而爲了方便計算本線程的讀鎖次數以及釋放掉鎖,需要在ThreadLocal中維護一個變量。這就是HoldCounter。源碼下半部分的基本做的事情就是在讓HoldCounter加一。- 讀鎖的釋放在內部同步器Sync的tryReleaseShared方法中
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//如果當前線程是第一個讀者
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
// 如果第一個讀者的讀鎖已經爲1 那麼第一個讀者置爲null
if (firstReaderHoldCount == 1)
firstReader = null;
else
//第一個讀者讀鎖數量減一
firstReaderHoldCount--;
} else {
//否則從緩存中獲取當前線程的讀鎖數量
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;//讀鎖數量減一
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;// state讀鎖狀態減一
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0; //如果state爲0,表示無鎖狀態,返回true
}
}