1. 簡介
重入鎖ReentrantLock
是排他鎖,排他鎖在同一時刻僅有一個線程可以進行訪問,但是在大多數場景下,大部分時間都是提供讀服務,而寫服務佔有的時間較少。然而讀服務不存在數據競爭問題,如果一個線程在讀時禁止其他線程讀勢必會導致性能降低。所以就提供了讀寫鎖。 讀寫鎖維護着一對鎖,一個讀鎖和一個寫鎖。通過分離讀鎖和寫鎖,使得併發性比一般的排他鎖有了較大的提升:在同一時間可以允許多個讀線程同時訪問,但是在寫線程訪問時,所有讀線程和寫線程都會被阻塞。
讀寫鎖ReentrantReadWriteLock
實現接口ReadWriteLock
,該接口維護了一對相關的鎖,一個用於只讀操作,另一個用於寫入操作。只要沒有writer
,讀取鎖可以由多個 reader
線程同時保持。寫入鎖是獨佔的。
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
讀寫鎖的主要特性:
- 公平性:支持公平性和非公平性。
- 重入性:支持重入。讀寫鎖最多支持65535個遞歸寫入鎖和65535個遞歸讀取鎖。
- 鎖降級:遵循獲取寫鎖、獲取讀鎖在釋放寫鎖的次序,寫鎖能夠降級成爲讀鎖。
ReadWriteLock
定義了兩個方法。readLock()
返回用於讀操作的鎖,writeLock()
返回用於寫操作的鎖。
ReentrantReadWriteLock
定義如下:
/** 內部類 讀鎖 */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** 內部類 寫鎖 */
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
/** 使用默認(非公平)的排序屬性創建一個新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock() {
this(false);
}
/** 使用給定的公平策略創建一個新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
/** 返回用於寫入操作的鎖 */
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
/** 返回用於讀取操作的鎖 */
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
abstract static class Sync extends AbstractQueuedSynchronizer {
/**
* 省略其餘源代碼
*/
}
public static class WriteLock implements Lock, java.io.Serializable{
/**
* 省略其餘源代碼
*/
}
public static class ReadLock implements Lock, java.io.Serializable {
/**
* 省略其餘源代碼
*/
}
ReentrantReadWriteLock
與ReentrantLock
一樣,其鎖主體依然是Sync
,它的讀鎖、寫鎖都是依靠Sync
來實現的。所以ReentrantReadWriteLock
實際上只有一個鎖,只是在獲取讀取鎖和寫入鎖的方式上不一樣而已,它的讀寫鎖其實就是兩個類:ReadLock
、WriteLock
,這兩個類都是Lock
實現。
在ReentrantLock
中使用一個int
類型的state
來表示同步狀態,該值表示鎖被一個線程重複獲取的次數。但是讀寫鎖ReentrantReadWriteLock
內部維護着一對鎖,需要用一個變量維護多種狀態。
所以讀寫鎖採用“按位切割使用”的方式來維護這個變量,將其切分爲兩部分,高16位表示讀,低16位表示寫。分割之後,讀寫鎖是如何迅速確定讀鎖和寫鎖的狀態呢?通過爲運算。假如當前同步狀態爲S
,那麼讀狀態等於S >>> 16
(無符號補0
右移16
位),寫狀態等於 S & 0x0000FFFF
(將高16位全部抹去)。代碼如下:
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;
static int sharedCount(int c) { return c >>> SHARED_SHIFT; } // 讀鎖的重入數
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } // 寫鎖的重入數
2. 寫鎖
寫鎖就是一個支持可重入的排他鎖。
2.1 寫鎖的獲取
寫鎖的獲取最終會調用tryAcquire(int arg)
,該方法在內部類Sync
中實現:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
// 獲取狀態
int c = getState();
// 獲取寫鎖的重入數
int w = exclusiveCount(c);
// 寫鎖或讀鎖至少有一個已被獲取
if (c != 0) {
//c != 0 && w == 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;
}
// 到這裏說明此時c=0,讀鎖和寫鎖都沒有被獲取
// 是否需要阻塞
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//設置獲取鎖的線程爲當前線程
setExclusiveOwnerThread(current);
return true;
}
該方法和ReentrantLock
的tryAcquire(int arg)
大致一樣,在判斷重入時增加了一項條件:讀鎖是否存在。因爲要確保寫鎖的操作對讀鎖是可見的,如果在存在讀鎖的情況下允許獲取寫鎖,那麼那些已經獲取讀鎖的其他線程可能就無法感知當前寫線程的操作。因此只有等讀鎖完全釋放後,寫鎖才能夠被當前線程所獲取,一旦寫鎖獲取了,所有其他讀、寫線程均會被阻塞。
2.2 寫鎖的釋放
獲取了寫鎖用完了則需要釋放,WriteLock
提供了unlock()
方法釋放寫鎖:
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
寫鎖的釋放最終還是會調用AQS
的模板方法release(int arg)
方法,該方法首先調用tryRelease(int arg)
方法嘗試釋放鎖,tryRelease(int arg)
方法爲讀寫鎖內部類Sync
中定義了,如下:
protected final boolean tryRelease(int releases) {
// 如果當前線程不是鎖的擁有者
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 寫鎖的新重入數
int nextc = getState() - releases;
// 如果獨佔模式重入數爲0了,說明獨佔模式被釋放
boolean free = exclusiveCount(nextc) == 0;
if (free)
// 若寫鎖的新線程數爲0,則將鎖的持有者設置爲null
setExclusiveOwnerThread(null);
// 設置寫鎖的新線程數
// 不管獨佔模式是否被釋放,更新獨佔重入數
setState(nextc);
return free;
}
3. 讀鎖
讀鎖爲一個可重入的共享鎖,它能夠被多個線程同時持有,在沒有其他寫線程訪問時,讀鎖總是或獲取成功。
3.1 讀鎖的獲取
讀鎖的獲取可以通過ReadLock
的lock()
方法:
public void lock() {
sync.acquireShared(1);
}
Sync
的acquireShared(int arg)
定義在AQS
中:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
tryAcqurireShared(int arg)
嘗試獲取讀同步狀態,該方法主要用於獲取共享式同步狀態,獲取成功返回 >= 0
的返回結果,否則返回< 0
的返回結果。
protected final int tryAcquireShared(int unused) {
//當前線程
Thread current = Thread.currentThread();
int c = getState();
//exclusiveCount(c)計算寫鎖
//如果存在寫鎖,且鎖的持有者不是當前線程,直接返回-1
//存在鎖降級問題,後續闡述
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//讀鎖
int r = sharedCount(c);
/*
* readerShouldBlock():讀鎖是否需要等待(公平鎖原則)
* r < MAX_COUNT:持有線程小於最大數(65535)
* compareAndSetState(c, c + SHARED_UNIT):設置讀取鎖狀態
*/
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
/*
* holdCount部分後面講解
*/
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);
}
讀鎖獲取的過程相對於獨佔鎖而言會稍微複雜下,整個過程如下:
1、因爲存在鎖降級情況,如果存在寫鎖且鎖的持有者不是當前線程則直接返回失敗,否則繼續
2、依據公平性原則,判斷讀鎖是否需要阻塞,讀鎖持有線程數小於最大值(65535
),且設置鎖狀態成功,執行以下代碼(對於HoldCounter
下面再闡述),並返回1
。如果不滿足該條件,執行fullTryAcquireShared()
。
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
//鎖降級
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
}
//讀鎖需要阻塞
else if (readerShouldBlock()) {
//列頭爲當前線程
if (firstReader == current) {
}
//HoldCounter後面講解
else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
//讀鎖超出最大範圍
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//CAS設置讀鎖成功
if (compareAndSetState(c, c + SHARED_UNIT)) {
//如果是第1次獲取“讀取鎖”,則更新firstReader和firstReaderHoldCount
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
}
//如果想要獲取鎖的線程(current)是第1個獲取鎖(firstReader)的線程,則將
//firstReaderHoldCount+1
else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
//更新線程的獲取“讀取鎖”的共享計數
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
fullTryAcquireShared(Thread current)
會根據“是否需要阻塞等待”,“讀取鎖的共享計數是否超過限制”等等進行處理。如果不需要阻塞等待,並且鎖的共享計數沒有超過限制,則通過CAS
嘗試獲取鎖,並返回1。
3.2 讀鎖的釋放
與寫鎖相同,讀鎖也提供了unlock()
釋放讀鎖:
public void unlock() {
sync.releaseShared(1);
}
unlcok()
方法內部使用Sync
的releaseShared(int arg)
方法,該方法定義在AQS
中:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
調用tryReleaseShared(int arg)
嘗試釋放讀鎖,該方法定義在讀寫鎖的Sync
內部類中:
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//如果想要釋放鎖的線程爲第一個獲取鎖的線程
if (firstReader == current) {
//僅獲取了一次,則需要將firstReader 設置null,否則 firstReaderHoldCount - 1
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
}
//獲取rh對象,並更新“當前線程獲取鎖的信息”
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;
}
//CAS更新同步狀態
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
3.3 HoldCounter
在讀鎖獲取鎖和釋放鎖的過程中,我們一直都可以看到一個變量rh
(HoldCounter
),該變量在讀鎖中扮演着非常重要的作用。
我們瞭解讀鎖的內在機制其實就是一個共享鎖,爲了更好理解HoldCounter
,我們暫且認爲它不是一個鎖的概率,而相當於一個計數器。一次共享鎖的操作就相當於在該計數器的操作。獲取共享鎖,則該計數器 + 1,釋放共享鎖,該計數器 – 1。只有當線程獲取共享鎖後才能對共享鎖進行釋放、重入操作。所以HoldCounter的作用就是當前線程持有共享鎖的數量,這個數量必須要與線程綁定在一起,否則操作其他線程鎖就會拋出異常。我們先看HoldCounter的定義:
static final class HoldCounter {
int count = 0;
final long tid = getThreadId(Thread.currentThread());
}
HoldCounter
定義非常簡單,就是一個計數器count
和線程id tid
兩個變量。按照這個意思我們看到HoldCounter
是需要和某給線程進行綁定了,我們知道如果要將一個對象和線程綁定僅僅有tid
是不夠的,而且從上面的代碼我們可以看到HoldCounter
僅僅只是記錄了tid
,根本起不到綁定線程的作用。
那麼怎麼實現呢?答案是ThreadLocal
,定義如下:
static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
通過上面代碼HoldCounter
就可以與線程進行綁定了。故而,HoldCounter
應該就是綁定線程上的一個計數器,而ThradLocalHoldCounter
則是線程綁定的ThreadLocal
。
從上面我們可以看到ThreadLocal
將HoldCounter
綁定到當前線程上,同時HoldCounter
也持有線程Id
,這樣在釋放鎖的時候才能知道ReadWriteLock
裏面緩存的上一個讀取線程(cachedHoldCounter
)是否是當前線程。這樣做的好處是可以減少ThreadLocal.get()
的次數,因爲這也是一個耗時操作。
需要說明的是這樣HoldCounter
綁定線程id
而不綁定線程對象的原因是避免HoldCounter
和ThreadLocal
互相綁定而GC
難以釋放它們(儘管GC
能夠智能的發現這種引用而回收它們,但是這需要一定的代價),所以其實這樣做只是爲了幫助GC
快速回收對象而已。看到這裏我們明白了HoldCounter
作用了,我們在看一個獲取讀鎖的代碼段:
else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
這段代碼涉及了幾個變量:firstReader
、firstReaderHoldCount
、cachedHoldCounter
。我們先理清楚這幾個變量:
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
private transient HoldCounter cachedHoldCounter;
firstReader
看名字就明白了爲第一個獲取讀鎖的線程,firstReaderHoldCount
爲第一個獲取讀鎖的重入數,cachedHoldCounter
爲HoldCounter
的緩存。清楚上面所有的變量了,HoldCounter
也明白了,我們就來給上面那段代碼標明註釋,如下:
//如果獲取讀鎖的線程爲第一次獲取讀鎖的線程,則firstReaderHoldCount重入數 + 1
else if (firstReader == current) {
firstReaderHoldCount++;
} else {
//非firstReader計數
if (rh == null)
rh = cachedHoldCounter;
//rh == null 或者 rh.tid != current.getId(),需要獲取rh
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
//加入到readHolds中
else if (rh.count == 0)
readHolds.set(rh);
//計數+1
rh.count++;
cachedHoldCounter = rh; // cache for release
}
這裏解釋下爲何要引入firstRead
、firstReaderHoldCount
。這是爲了一個效率問題,firstReader
是不會放入到readHolds
中的,如果讀鎖僅有一個的情況下就會避免查找readHolds
。
4. 鎖降級
讀寫鎖有一個特性就是鎖降級,鎖降級就意味着寫鎖是可以降級爲讀鎖的,但是需要遵循先獲取寫鎖、獲取讀鎖在釋放寫鎖的次序。注意如果當前線程先獲取寫鎖,然後釋放寫鎖,再獲取讀鎖這個過程不能稱之爲鎖降級,鎖降級一定要遵循那個次序。在獲取讀鎖的方法tryAcquireShared(int unused)
中,有一段代碼就是來判讀鎖降級的:
int c = getState();
//exclusiveCount(c)計算寫鎖
//如果存在寫鎖,且鎖的持有者不是當前線程,直接返回-1
//存在鎖降級問題,後續闡述
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
//讀鎖
int r = sharedCount(c);
鎖降級中讀鎖的獲取釋放爲必要?肯定是必要的。試想,假如當前線程A
不獲取讀鎖而是直接釋放了寫鎖,這個時候另外一個線程B
獲取了寫鎖,那麼這個線程B
對數據的修改是不會對當前線程A
可見的。如果獲取了讀鎖,則線程B
在獲取寫鎖過程中判斷如果有讀鎖還沒有釋放則會被阻塞,只有當前線程A
釋放讀鎖後,線程B
纔會獲取寫鎖成功。
所以,獲取讀鎖時,如果存在寫鎖,那麼讀鎖並不一定是不能獲得的。
鎖降級示例:
public void processDate(){
readLock.lock();
if(!update){
// 必須先釋放讀鎖
readLock.unlock();
// 鎖降級從寫鎖被獲取到開始
writeLock.lock();
try{
if(!update()){
// 準備數據的流程
update = true;
}
readLock.lock();
}finally{
writeLock.unlock();
}
// 鎖降級完成,寫鎖降級爲讀鎖
}
try{
// 使用數據的流程
}finally{
readLock.unlock();
}
}
5. 示例
/**
* @author wangzhao
* @date 2019/9/22 9:53
*/
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();
}
}
}