對於多個線程共享同一個資源的時候,多個線程同時對共享資源做讀操作是不會發生線程安全性問題的,但是一旦有一個線程對共享數據做寫操作其他的線程再來讀寫共享資源的話,就會發生數據安全性問題,所以出現了讀寫鎖ReentrantReadWriteLock。讀寫鎖允許多個線程同時獲取讀鎖,但有一個線程獲取寫鎖之後其他線程都會進入等待隊列進行等待。
讀寫鎖的寫鎖是一把獨佔鎖,它與ReentrantLock的原理十分相似,可以參靠https://blog.csdn.net/qq_37685457/article/details/89704124,這裏就不解釋寫鎖了,來說說讀鎖。
因爲讀寫鎖需要用一個int類型的值來保存讀鎖和寫鎖兩把鎖的數量,所以要將int類型的值進行拆分存儲,int類型佔4個字節,每個字節佔8位,所以一個int類型的變量佔32位,將高16用來保存讀鎖的數量,低16位保存寫鎖的數量。
讀寫鎖的讀鎖原理:如果有線程想申請讀鎖的話,首先會判斷寫鎖是否被持有,如果寫鎖被持有且當前線程並不是持有寫鎖的線程,那麼就會返回-1,獲取鎖失敗,進入到等待隊列等待。如果寫鎖未被線程所持有或者當前線程和持有寫鎖的線程是同一線程的話就會開始獲取讀鎖。線程首先會判斷讀鎖的數量是否超過65535個,如果沒超過就CAS修改state變量的高16位的值,也就是將state的值+1,如果這個步驟失敗的話它會循環這個操作,直到成功爲止。CAS修改成功之後,代表讀鎖獲取成功,會判斷一下當前線程是否是第一次讀線程,如果是,就設置第一次多線程和第一次讀計數器(爲了性能和可重入)。如果不是第一次獲取讀鎖,判斷一下是否是與第一次讀線程相同,如果與第一次讀線程是同一線程就將第一次讀計數器+1。如果也不是第一次讀線程,判斷一下是否是最後一次讀線程,如果是就將最後一次讀計數器+1。如果都不是,就新建一個計數器,設置最後一次讀線程爲自己本身線程,然後刷新它的讀計數器。
讀鎖源碼:
protected final int11 tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// exclusiveCount(c) != 0 ---》 用 state & 65535 得到低 16 位的值。如果不是0,說明寫鎖別持有了。
// getExclusiveOwnerThread() != current----> 不是當前線程
// 如果寫鎖被霸佔了,且持有線程不是當前線程,返回 false,加入隊列。獲取寫鎖失敗。
// 反之,如果持有寫鎖的是當前線程,就可以繼續獲取讀鎖了。
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
// 獲取鎖失敗
return -1;
// 如果寫鎖沒有被霸佔,則將高16位移到低16位。
int r = sharedCount(c);// c >>> 16
// !readerShouldBlock() 和寫鎖的邏輯一樣(根據公平與否策略和隊列是否含有等待節點)
// 不能大於 65535,且 CAS 修改成功
if (!readerShouldBlock() && r < 65535 && compareAndSetState(c, c + 65536)) {
// 如果讀鎖是空閒的, 獲取鎖成功。
if (r == 0) {
// 將當前線程設置爲第一個讀鎖線程
firstReader = current;
// 計數器爲1
firstReaderHoldCount = 1;
}// 如果讀鎖不是空閒的,且第一個讀線程是當前線程。獲取鎖成功。
else if (firstReader == current) {//
// 將計數器加一
firstReaderHoldCount++;
} else {// 如果不是第一個線程,獲取鎖成功。
// cachedHoldCounter 代表的是最後一個獲取讀鎖的線程的計數器。
HoldCounter rh = cachedHoldCounter;
// 如果最後一個線程計數器是 null 或者不是當前線程,那麼就新建一個 HoldCounter 對象
if (rh == null || rh.tid != getThreadId(current))
// 給當前線程新建一個 HoldCounter
cachedHoldCounter = rh = readHolds.get();
// 如果不是 null,且 count 是 0,就將上個線程的 HoldCounter 覆蓋本地的。
else if (rh.count == 0)
readHolds.set(rh);
// 對 count 加一
rh.count++;
}
return 1;
}
// 死循環獲取讀鎖。包含鎖降級策略。
return fullTryAcquireShared(current);
}
源碼還是比較多,附個圖吧,簡單明瞭
讀寫鎖還存在一個鎖降級的概念,就是在釋放寫鎖之前可以將鎖降級爲讀鎖,但是讀鎖不可升級爲寫鎖。鎖降級的目的一直都存在爭議,但其實鎖降級就是一種特殊的可重入操作。存在一直情況,如果當前線程已經持有寫鎖,但如果當前線程再來獲取讀鎖的話應該是允許的,但如果寫鎖不釋放的話,就算是本身線程來獲取讀鎖也要進行等待,這樣是不合理的,所以說它允許鎖降級爲讀鎖,這樣就可以不用等待直接獲取讀鎖了,也是爲了效率的一種體現。