簡介
ReentrantReadWriteLock 從字面意思可以看出,是和重入、讀寫有關係的鎖,實際上 ReentrantReadWriteLock 確實也是支援可重入的讀寫鎖,並且支援公平和非公平獲取鎖兩種模式。
為什麼會出現讀寫鎖?
普通鎖可以保證共享資料在同一時刻只被一個執行緒訪問,就算有多個執行緒都只是讀取的操作,也還是要排隊等待獲取鎖,我們知道資料如果只涉及到讀操作,是不會出現執行緒安全方面的問題的,那這部分加鎖是不是可以去掉?或者是加鎖不互斥?如果在讀多寫少的情況下,使用普通的鎖,在所有讀的情況加鎖互斥等待會是一個及其影響系統併發量的問題,如果所有的讀操作不互斥,只有涉及到寫的時候才互斥,這樣會不會大大的提高併發量呢?答案是肯定的,ReentrantReadWriteLock 就是這樣乾的,讀讀不互斥,讀寫、寫讀、寫寫都是互斥的,可以大大提高系統併發量。
原始碼分析
類結構
ReentrantReadWriteLock 僅實現了ReadWriteLock介面
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {...}
ReadWriteLock 介面僅有兩個方法,分別是 readLock()
和 writeLock()
;
主要屬性
ReentrantReadWriteLock 有3個重要的屬性,分別是讀鎖readerLock,寫鎖writerLock和同步器sync,原始碼如下:
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
主要內部類
-
Sync:同步器,繼承至AbstractQueuedSynchronizer,定義了兩個抽象方法,用於兩種模式下自定義實現判斷是否要阻塞
abstract static class Sync extends AbstractQueuedSynchronizer{ ... abstract boolean readerShouldBlock(); abstract boolean writerShouldBlock(); ... }
-
NonfairSync:非公平同步器,用於實現非公平鎖,繼承Sync
static final class NonfairSync extends Sync {...}
-
FairSync:公平同步器,用於實現公平鎖,繼承Sync
static final class FairSync extends Sync {...}
-
ReadLock:讀鎖,實現了Lock介面,持有同步器Sync的具體例項
public static class ReadLock implements Lock, java.io.Serializable { ... private final Sync sync; ... }
-
WriteLock:寫鎖,實現了Lock介面,持有同步器Sync的具體例項
public static class WriteLock implements Lock, java.io.Serializable { ... private final Sync sync; ... }
構造方法
有兩個預設的構造方法,無參預設採用非公平鎖,有參傳入true使用公平鎖
public ReentrantReadWriteLock() {
this(false);
}
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; }
獲取讀鎖:readLock.lock()
讀鎖主要是按照共享模式來獲取鎖的,在前面講AQS的例子中——基於AQS實現自己的共享鎖,也是差不多的流程,只不過不同的鎖的實現方法tryAcquireShared有一定的區別。ReentrantReadWriteLock 讀鎖獲取過程原始碼如下:
public void lock() {
// 共享模式獲取鎖
sync.acquireShared(1);
}
// acquireShared 是AQS框架裡面的程式碼
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
// tryAcquireShared 是RRWLock.Sync 裡面的自己實現,所以這裡沒有公平和非公平所謂之稱
protected final int tryAcquireShared(int unused) {
// 當前想要獲得鎖的執行緒
Thread current = Thread.currentThread();
// 獲取state值
int c = getState();
// 獨佔鎖被佔用了,並且不是當前執行緒佔有的,返回-1,出去要排隊
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
// 讀鎖共享鎖的次數
int r = sharedCount(c);
// 判斷讀是否要阻塞,讀共享鎖的次數是否超過最大值,CAS 更新鎖state值
// readerShouldBlock 的返回要根據同步器是否公平的具體實現來決定
if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
// r==0, 設定第一次獲得讀鎖的讀者
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 持有第一個讀者讀鎖的執行緒重入計數
firstReaderHoldCount++;
} else {
// 除第一個執行緒之後的其他執行緒獲得讀鎖
// 每個執行緒每次獲得讀鎖重入計數+1
// readHolds 就是一個ThreadLocal,裡面放的HoldCounter,用來統計每個執行緒的重入次數
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
// 獲得讀鎖,返回1
return 1;
}
// 上面if分支沒進去時,走這裡嘗試獲取讀鎖
return fullTryAcquireShared(current);
}
上面程式碼中的readerShouldBlock()
方法有兩種情況下會返回true:
-
公平模式下,呼叫的
AQS.hasQueuedPredecessors()
方法static final class FairSync extends Sync { final boolean writerShouldBlock() { return hasQueuedPredecessors(); } final boolean readerShouldBlock() { return hasQueuedPredecessors(); } } public final boolean hasQueuedPredecessors() { Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; // head 頭結點是當前持有鎖的節點,它的下一個節點不是當前執行緒,返回true,表示應該要阻塞當前執行緒 return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
?上面程式碼的主要思想就是:看一下隊頭排隊等待獲取鎖的執行緒是不是當前執行緒,不是的話就應該要阻塞當前執行緒;
-
非公平模式下,最終呼叫的
AQS.apparentlyFirstQueuedIsExclusive()
方法static final class NonfairSync extends Sync { private static final long serialVersionUID = -8159625535654395037L; final boolean writerShouldBlock() { return false; // writers can always barge } final boolean readerShouldBlock() { return apparentlyFirstQueuedIsExclusive(); } } // apparentlyFirstQueuedIsExclusive 方法是AQS裡面的方法 final boolean apparentlyFirstQueuedIsExclusive() { // h 是同步佇列的頭結點,當前持有鎖的節點 // s 是下一個應該獲得鎖的節點 Node h, s; // s 節點如果不是共享模式(在RRWLock 裡面就是讀鎖的意思),s節點是排他模式(想要寫鎖)返回true, return (h = head) != null && (s = h.next) != null && !s.isShared() && s.thread != null; }
?上面程式碼的主要思想就是:看一下隊頭排隊等待獲取鎖的第一個執行緒是不是要獲取寫鎖,如果是就返回true,表示要阻塞當前執行緒,當前執行緒前面還有個要獲得寫鎖的執行緒在排隊呢!如果存在這種情況,其他獲取讀鎖的執行緒都要給這種情況讓路(寫鎖優先順序更高)。那如果佇列中第一個執行緒不是要獲取寫鎖,那既然都是獲取讀鎖,那就無所謂了,允許你插隊。
上面的if分支進入失敗時,會進入到fullTryAcquireShared()
方法再次嘗試獲得讀鎖有3種情況會進入到這個方法:
readerShouldBlock()
方法返回true,上面已經分析了,這個方法什麼時候會返回true- 共享計數達到了最大值 MAX_COUNT(65535),可能性較小
- CAS 修改state 值失敗,也就是獲取鎖失敗
下面是 fullTryAcquireShared() 方法的分析:
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
// 自旋
for (;;) {
int c = getState();
// != 0 已經有其他執行緒獲得了寫鎖
if (exclusiveCount(c) != 0) {
// 如果不是當前執行緒獲得的寫鎖,返回-1,出去阻塞排隊
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) {
// 要進入到這個分支,說明exclusiveCount(c) == 0 , 也就是寫鎖沒被佔用
// readerShouldBlock() == true , 公平模式下,同步佇列中有其他執行緒在排隊,非公平模式下,有即將要獲得寫鎖的執行緒
// readerShouldBlock() 返回true ,也就是要阻塞當前執行緒的意思
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
// 進入到這裡,說明第一個讀鎖不是當前執行緒獲得的
// rh 可以理解為當前執行緒的重入計數
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
// 返回-1,阻塞當前執行緒,出去排隊
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
// 超讀鎖上限,丟擲錯誤
throw new Error("Maximum lock count exceeded");
// 進入到這兒,說明執行緒沒有其他執行緒獲得了寫鎖,並且不需要阻塞當前執行緒
// 再次嘗試CAS 獲得鎖,CAS 修改失敗會繼續自旋進行
if (compareAndSetState(c, c + SHARED_UNIT)) {
// 成功獲得鎖
if (sharedCount(c) == 0) {
// 第一個獲得讀鎖的執行緒
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 第一個獲得讀鎖的執行緒重入計數+1
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++;
// 快取成功獲取readLock的最後一個執行緒的計數
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
如果上面fullTryAcquireShared()方法還是沒有獲得鎖,返回-1,就會進入下面的doAcquireShared(int arg)
方法:
// doAcquireShared 方法是AQS裡面的程式碼,非RRWLock 實現
private void doAcquireShared(int arg) {
// 新增一個共享模式的節點到同步佇列,並返回當前節點
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
// 中斷標識
boolean interrupted = false;
// for迴圈自旋操作
for (;;) {
// 在同步佇列中,當前節點的前驅結點
final Node p = node.predecessor();
if (p == head) {
// 如果前驅結點是頭結點,說明排隊輪到當前節點獲得鎖
// tryAcquireShared 再次嘗試獲取鎖,上面的邏輯一模一樣
int r = tryAcquireShared(arg);
if (r >= 0) {
// >=0 說明成功獲得了鎖
// 設定新的頭結點,並檢查後面是否是在獲得讀鎖,如果是就喚醒它
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
// 阻塞期間執行緒被中斷了
selfInterrupt();
failed = false;
return;
}
}
// 阻塞中斷執行緒
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
// 舊的頭結點
Node h = head; // Record old head for check below
// 獲得鎖的執行緒節點設定為新的頭結點
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 檢查獲得鎖的下一個節點s是否是共享模式的節點(讀)
if (s == null || s.isShared())
doReleaseShared();
}
}
private void doReleaseShared() {
// 自旋
for (;;) {
Node h = head;
// 同步佇列不為空
if (h != null && h != tail) {
int ws = h.waitStatus;
// -1 :表示當前節點的後繼節點包含的執行緒需要執行,也就是unpark
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 喚醒被阻塞的下一個節點
unparkSuccessor(h);
} else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 只會喚醒一個節點,在呼叫上面程式碼過程中,如果head節點變了,就會一直自旋,直到成功
if (h == head) // loop if head changed
break;
}
}
獲取讀鎖過程總結
- 嘗試去獲取鎖
tryAcquireShared()
- 在
tryAcquireShared()
中成功獲得鎖,就直接退出,執行lock() 之後的程式碼邏輯- 如果有其他執行緒已經佔用了寫鎖,退出方法,返回-1,獲取鎖失敗
- 檢查是否要阻塞當前的執行緒
readerShouldBlock()
,有兩種情況下(也就是公平鎖和非公平鎖獲取讀鎖的區別)會阻塞當前現在:- 如果是公平鎖,會看一下隊頭排隊等待獲取鎖的執行緒是不是當前執行緒,不是的話就應該要阻塞當前執行緒;公平模式下是不允許插隊的!
- 如果是非公平鎖,看一下隊頭排隊等待獲取鎖的第一個執行緒是不是要獲取寫鎖,如果是表示要阻塞當前執行緒,寫鎖優先順序更高!
- 檢查讀鎖計數是否已經到了最大值(65535)
- 上面檢查通過,才嘗試CAS 修改同步狀態,修改成功,代表成功獲取讀鎖,退出方法返回1
- 成功獲取讀鎖,如果是第一個獲得讀鎖的執行緒,會快取該執行緒
firstReader
,如果是重入,會進行重入計數,如果是新的執行緒獲得讀鎖,會用一個ThreadLocal來儲存重入計數
- 成功獲取讀鎖,如果是第一個獲得讀鎖的執行緒,會快取該執行緒
- 如果到上面還沒獲取到鎖(可能是CAS修改同步狀態失敗),會進行自旋繼續嘗試獲取鎖,對應方法
fullTryAcquireShared()
,該方法要麼獲取鎖成功,要麼獲取鎖失敗,直到退出整個tryAcquireShared()
方法
- 如果
tryAcquireShared()
中沒有獲得鎖,進入到AQS的doAcquireShared
方法,排隊、阻塞執行緒doAcquireShared
方法也是一個自旋的操作,沒有獲取到鎖,就會阻塞執行緒,等待被喚醒後繼續獲取鎖,知道獲取鎖成功為止
釋放讀鎖:readLock.lock()
讀鎖釋放鎖的邏輯如下:
public void unlock() {
// 開始釋放讀鎖
sync.releaseShared(1);
}
//AQS框架中 的方法
public final boolean releaseShared(int arg) {
// tryReleaseShared 在RRWLock 中的Sync裡面
if (tryReleaseShared(arg)) {
// 喚醒後面的讀鎖節點
doReleaseShared();
return true;
}
return false;
}
// RRWLock.Sync 的實現方法
protected final boolean tryReleaseShared(int unused) {
// 當前執行緒
Thread current = Thread.currentThread();
if (firstReader == current) {
// 第一個讀鎖執行緒
if (firstReaderHoldCount == 1)
// 如果它只獲得了一次鎖,直接置為null
firstReader = null;
else
// 第一個執行緒獲得讀鎖,並且重入獲取鎖很多次,慢慢減,直到為1,置為null
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;
}
// 上面只是減重入的計數
// 下面是自旋,重置同步狀態state值
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// CAS 修改成功,並且要state為0才是真正釋放了讀鎖
// 如果有重入,只有釋放最後一次才會返回true, 之後才會去嘗試喚醒之後的節點
return nextc == 0;
}
}
private void doReleaseShared() {
// 自旋
for (;;) {
Node h = head;
// 同步等待的佇列不為空
if (h != null && h != tail) {
int ws = h.waitStatus;
// 檢查狀態是否要喚醒下一個節點的執行緒
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 加入h節點是持有鎖的節點,會喚醒它的下一個節點執行緒
unparkSuccessor(h);
} else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 理論上喚醒一個就會退出
if (h == head) // loop if head changed
break;
}
}
釋放讀鎖過程總結:
- 減計數,包含執行緒重入獲取鎖的計數
- 從這裡可以看出一個執行緒存在多次釋放鎖,會丟擲異常
- 自旋,CAS 修改同步狀態,重入獲取鎖的執行緒只有在state等於0時才是真正的釋放鎖成功
- 釋放鎖成功後,會喚醒佇列中的下一個節點,下一個節點會繼續獲取鎖
獲取寫鎖:writeLock.lock()
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
// 當前執行緒
Thread current = Thread.currentThread();
int c = getState();
// 寫鎖計數,>0的話說明寫鎖已經被佔用了
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
// c != 0 and w == 0 可能共享鎖已經被佔用了,這時候寫鎖獲取失敗
// 同一個執行緒先獲取讀鎖,再獲取寫鎖,也會在這裡返回false,獲取寫鎖出去之後會阻塞自己,
// 然後自己的讀鎖也不會釋放,其他執行緒也獲取不了讀鎖,就出現了死鎖
if (w == 0 || current != getExclusiveOwnerThread())
// c != 0 and w == 0 鎖的持有者不是當前執行緒,返回false
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
// 超限了 65535
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
// 重入獲取鎖,計數+1
setState(c + acquires);
return true;
}
// writerShouldBlock的實現程式碼,以看上面讀鎖獲取readerShouldBlock的分析
// 公平鎖時,writerShouldBlock 呼叫的hasQueuedPredecessors()
// 非公平鎖時,只返回false
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
// CAS 修改失敗,返回false
return false;
// 成功獲取寫鎖,設定鎖的擁有者執行緒
setExclusiveOwnerThread(current);
return true;
}
如果上面方法沒有獲取到寫鎖,會執行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
,這塊的程式碼分析,可以檢視之前的文章,關於AQS的分析或者ReentrantLock的分析。
釋放寫鎖:writeLock.unlock()
釋放寫鎖的邏輯比較簡單,一般加鎖和解鎖都是成對出現的,所以這裡解鎖並不需要同步互斥的手段來進行,原始碼如下:
public void unlock() {
sync.release(1);
}
// AQS 框架的程式碼
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
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;
}
完全釋放鎖成功後,喚醒下一個節點的邏輯在AQS的unparkSuccessor程式碼中,不需要RRWLock來實現。
死鎖問題
在上面獲取寫鎖的過程中,分析了同一個執行緒先獲取讀鎖,再獲取寫鎖,寫鎖的邏輯會阻塞自己的執行緒,但是寫鎖和讀鎖又是同一個執行緒,相當於前面的寫鎖也被阻塞了,這時候寫鎖沒地方釋放,讀鎖也沒有地方釋放,其他執行緒讀鎖和寫鎖也都獲取不了了,因為前面有個寫鎖在排隊獲取。
public static void main(String[] args) throws InterruptedException{
ReentrantReadWriteLock lock = new ReentrantReadWriteLock(false);
Lock writeLock = lock.writeLock();
Lock readLock = lock.readLock();
new Thread(new Runnable(){
@SneakyThrows
@Override
public void run(){
TimeUnit.SECONDS.sleep(1);
// 模擬1秒後其他執行緒來獲得讀鎖
System.out.println(Thread.currentThread().getName()+":準備獲得讀鎖");
readLock.lock();
System.out.println(Thread.currentThread().getName()+":執行緒獲得讀鎖");
readLock.unlock();
System.out.println(Thread.currentThread().getName()+":釋放了讀鎖");
}
},"T0").start();
readLock.lock();
System.out.println(Thread.currentThread().getName()+":獲得了讀鎖");
writeLock.lock();
System.out.println(Thread.currentThread().getName()+":獲得了寫鎖");
readLock.unlock();
System.out.println(Thread.currentThread().getName()+":解讀鎖");
writeLock.unlock();
System.out.println(Thread.currentThread().getName()+":解寫鎖");
}
輸出結果:
main:獲得了讀鎖
T0:準備獲得讀鎖
從上面輸出結果可以看出,只有main執行緒獲得了讀鎖,自己獲取寫鎖被阻塞,其他執行緒也獲取不了讀鎖,最後產生了死鎖。
寫執行緒飢餓問題
ReentrantReadWriteLock 的讀寫是互斥的,意思就是讀鎖在獲取鎖後,在還沒有釋放鎖的期間,獲取寫鎖的程式來了也要阻塞自己排隊,如果有大量的執行緒獲取了讀鎖,之後有一個執行緒獲取寫鎖,寫鎖就可能一直獲取不到寫鎖,引起寫鎖執行緒“飢餓”,這就是RRWLock的寫執行緒飢餓問題。
我們用程式碼來驗證一下上面的結論:
private static void testWriteLockHunger() throws InterruptedException{
ReentrantReadWriteLock lock = new ReentrantReadWriteLock(false);
Lock writeLock = lock.writeLock();
Lock readLock = lock.readLock();
// T0 執行緒先獲得讀鎖,並持有一段時間
new Thread(new Runnable(){
@SneakyThrows
@Override
public void run(){
readLock.lock();
System.out.println(Thread.currentThread().getName()+":最開始執行緒獲得讀鎖");
// 睡眠15秒,一直持有讀鎖
TimeUnit.SECONDS.sleep(15);
readLock.unlock();
System.out.println(Thread.currentThread().getName()+":釋放了讀鎖");
}
},"T0").start();
// 1秒後其他執行緒再來獲取鎖,保證前面那個T0執行緒最先獲得讀鎖
TimeUnit.SECONDS.sleep(1);
// TW-1 來排隊獲取寫鎖,是為了讓後面的讀鎖,寫鎖都入隊排隊
new Thread(new Runnable(){
@SneakyThrows
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+":準備獲得寫鎖");
writeLock.lock();
System.out.println(Thread.currentThread().getName()+":獲得寫鎖");
TimeUnit.SECONDS.sleep(5);
writeLock.unlock();
System.out.println(Thread.currentThread().getName()+":釋放了寫鎖");
}
},"TW-1").start();
TimeUnit.SECONDS.sleep(1);
// 這裡睡眠1秒是為了寫鎖排隊在讀鎖獲取的前面
IntStream.range(1,5).forEach(i->{
new Thread(new Runnable(){
@SneakyThrows
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+":準備獲取讀鎖");
readLock.lock();
System.out.println(Thread.currentThread().getName()+":獲取了讀鎖");
// 持有部分時間的讀鎖
TimeUnit.SECONDS.sleep(i*2);
readLock.unlock();
System.out.println(Thread.currentThread().getName()+":釋放了讀鎖");
}
},"T-"+i).start();
});
// 最後再來個獲取寫鎖的執行緒,肯定會在所有讀鎖的後面獲取到寫鎖
new Thread(new Runnable(){
@SneakyThrows
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+":準備獲取寫鎖");
writeLock.lock();
System.out.println(Thread.currentThread().getName()+":獲取了寫鎖");
// 持有部分時間的讀鎖
TimeUnit.SECONDS.sleep(2);
writeLock.unlock();
System.out.println(Thread.currentThread().getName()+":釋放了寫鎖");
}
},"TW").start();
}
上面程式碼輸出示例:
T0:最開始執行緒獲得讀鎖
TW-1:準備獲得寫鎖
T-1:準備獲取讀鎖
T-2:準備獲取讀鎖
T-4:準備獲取讀鎖
T-3:準備獲取讀鎖
TW:準備獲取寫鎖
T0:釋放了讀鎖
TW-1:獲得寫鎖
TW-1:釋放了寫鎖
T-1:獲取了讀鎖
T-2:獲取了讀鎖
T-4:獲取了讀鎖
T-3:獲取了讀鎖
T-1:釋放了讀鎖
T-2:釋放了讀鎖
T-3:釋放了讀鎖
T-4:釋放了讀鎖
TW:獲取了寫鎖
TW:釋放了寫鎖
從上面輸出結果可以看出,TW寫鎖是最後才獲取到寫鎖的,如果前面有大量的讀鎖在排隊的話,寫鎖肯定就會造成飢餓的。
如果不想讓獲取寫鎖的執行緒“飢餓”怎麼辦呢?
可以把最後獲取寫鎖的執行緒TW獲取鎖方式改造下,程式碼如下:
new Thread(new Runnable(){
@SneakyThrows
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+":準備獲取寫鎖");
while(!writeLock.tryLock()){
// 一直嘗試獲得寫鎖,直到成功
}
System.out.println(Thread.currentThread().getName()+":獲取了寫鎖");
// 持有部分時間的讀鎖
TimeUnit.SECONDS.sleep(2);
writeLock.unlock();
System.out.println(Thread.currentThread().getName()+":釋放了寫鎖");
}
},"TW").start();
測試輸出結果:
T0:最開始執行緒獲得讀鎖
TW-1:準備獲得寫鎖
T-1:準備獲取讀鎖
T-2:準備獲取讀鎖
T-3:準備獲取讀鎖
T-4:準備獲取讀鎖
TW:準備獲取寫鎖
T0:釋放了讀鎖
TW-1:獲得寫鎖
TW-1:釋放了寫鎖
TW:獲取了寫鎖
TW:釋放了寫鎖
T-4:獲取了讀鎖
T-2:獲取了讀鎖
T-3:獲取了讀鎖
T-1:獲取了讀鎖
T-1:釋放了讀鎖
T-2:釋放了讀鎖
T-3:釋放了讀鎖
T-4:釋放了讀鎖
從上面輸出結果可以看出,TW執行緒成功的在讀鎖前面獲取到了寫鎖;那為什麼會這樣呢?因為採用lock()來獲取鎖,如果第一次tryAcquire沒有獲取到鎖,就會被加入到佇列等待,只要進入了佇列,就只能按照佇列中的順序來獲得鎖了,而tryLock在獲取鎖失敗後是不會加入到同步等待佇列中去的,從而實現“插隊”的功能。
總結
- 讀寫鎖除了讀讀不互斥,讀寫、寫讀、寫寫都是互斥的。
- 讀寫互斥的意思是A執行緒先獲取讀鎖不釋放,B來獲取寫鎖,這時候B執行緒一樣的要阻塞自己
- 同一個執行緒先獲取讀鎖,再獲取寫鎖,會導致死鎖
- 允許同一個執行緒先獲取寫鎖,再獲取讀鎖;但是不允許同一個執行緒先獲取讀鎖,再獲取寫鎖;可以理解為允許鎖降級,不允許鎖升級。
- 公平鎖模式下,獲取寫鎖會去檢查佇列中是否有排隊更久的執行緒。
- 非公平鎖模式下,獲取寫鎖不會去檢查同步佇列中是否有排隊更久的執行緒。
- 公平鎖模式下,獲取讀鎖會去檢查佇列中是否有排隊更久的執行緒。
- 非公平鎖模式下,獲取讀鎖會去檢查佇列中第一個等待獲取的是不是寫鎖,如果存在就要阻塞當前獲取讀鎖的執行緒(寫鎖優先順序更高)。