聲明:本文爲作者原創,如若轉發,請指明轉發地址
文章目錄
1、ReentranReadWriteLock示例
當讀操作遠遠高於寫操作時,這時候使用 讀寫鎖 讓 讀-讀 可以併發,讀-寫和寫-寫互斥,提高性能。
提供一個 數據容器類 內部分別使用讀鎖保護數據的 read() 方法,寫鎖保護數據的 write() 方法
讀鎖不是獨佔式鎖,即同一時刻該鎖可以被多個讀線程獲取也就是一種共享式鎖。現共享式同步組件的同步語義需要通過重寫AQS的tryAcquireShared方法和tryReleaseShared方法。
@Slf4j(topic = "c.TestReadWriteLock")
public class TestReadWriteLock {
public static void main(String[] args) throws InterruptedException {
DataContainer dataContainer = new DataContainer();
new Thread(() -> {
dataContainer.read();
}, "t1").start();
new Thread(() -> {
dataContainer.read();
}, "t2").start();
}
}
@Slf4j(topic = "c.DataContainer")
class DataContainer {
private Object data;
private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock r = rw.readLock();
private ReentrantReadWriteLock.WriteLock w = rw.writeLock();
public Object read() {
log.debug("獲取讀鎖...");
r.lock();
try {
log.debug("讀取");
sleep(1);
return data;
} finally {
log.debug("釋放讀鎖...");
r.unlock();
}
}
public void write() {
log.debug("獲取寫鎖...");
w.lock();
try {
log.debug("寫入");
sleep(1);
} finally {
log.debug("釋放寫鎖...");
w.unlock();
}
}
}
結果:讀鎖沒有釋放時,其他線程就可以獲取讀鎖
09:29:07.134 c.DataContainer [t2] - 獲取讀鎖...
09:29:07.134 c.DataContainer [t1] - 獲取讀鎖...
09:29:07.134 c.DataContainer [t1] - 讀取
09:29:07.134 c.DataContainer [t2] - 讀取
09:29:08.146 c.DataContainer [t1] - 釋放讀鎖...
09:29:08.146 c.DataContainer [t2] - 釋放讀鎖...
2、ReentrantReadWriteLock底層原理
讀寫鎖用的是同一個 Sycn 同步器,因此等待隊列、state 等也是同一個
1、t1 w.lock,t2 r.lock
ReentrantReadWriteLock類結構:
1、寫鎖上鎖流程
1、acquire(arg)方法
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
//內部類 WriteLock類
public static class WriteLock implements Lock, java.io.Serializable {
private final Sync sync;
//通過WriteLock類對象w調用該方法
public void lock() {
sync.acquire(1);
}
}
}
2、tryAcquire(arg)方法
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer{
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
}
由於t1線程第第一個獲取鎖的線程,因此 t1 成功上鎖,流程與 ReentrantLock 加鎖相比沒有特殊之處,不同是寫鎖狀態佔了 state 的低 16 位,而讀鎖使用的是 state 的高 16 位
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
//1、獲取寫鎖當前的同步狀態,即鎖狀態的低16位
int c = getState();
//2、獲取寫鎖獲取的次數
int w = exclusiveCount(c);
//如果寫鎖狀態state!=0,說明寫鎖已經被其他線程獲取
if (c != 0) {
//如果獲取寫鎖的線程不是當前線程,獲取寫鎖失敗
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//寫鎖計數超過低 16 位, 報異常
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;
}
這裏需要注意一點:int w = exclusiveCount(c);
表示寫鎖獲取的次數
/**
該方法是獲取讀鎖被獲取的次數,是將同步狀態(int c)右移16次,即取同步狀態的高16位,
結論:同步狀態的高16位用來表示讀鎖被獲取的次數。
*/
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/**
將同步狀態(state爲int類型)與0x0000FFFF相與,即獲取同步狀態的低16位,即寫鎖被獲取的次數,
結論:同步狀態的低16位用來表示寫鎖的獲取次數。
*/
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
總結:同步狀態的低16位用來表示寫鎖的獲取次數,同步狀態的高16位用來表示讀鎖的獲取狀態。
當寫鎖已經被其他線程獲取,就返回false,繼續執行下面的邏輯。否則,獲取鎖成功並支持可重入鎖,更新獲取鎖的次數。
3、writerShouldBlock()方法
該方法時Sync類中的抽象方法,有公平鎖和非公平鎖兩種實現方式:
對於非公平鎖:
static final class NonfairSync extends Sync {
//對於非公平鎖總是返回false,不需要阻塞
final boolean writerShouldBlock() {
return false;
}
}
對於公平鎖:
static final class FairSync extends Sync {
//對於公平鎖,需要判斷
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
}
2、讀鎖上鎖流程
1、acquireShared(arg)方法
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
public static class ReadLock implements Lock, java.io.Serializable {
//...
//調用讀鎖的lock()方法
public void lock() {
sync.acquireShared(1);
}
}
2、tryAcquireShared(arg)方法
t2 執行 r.lock,這時進入讀鎖的 sync.acquireShared(1) 流程,首先會進入 tryAcquireShared 流程。如果有寫鎖佔據並且獲取寫鎖的線程不是當前線程,那麼 tryAcquireShared 返回 -1 表示失敗
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer {
public final void acquireShared(int arg) {
//tryAcquireShared 返回負數, 表示獲取讀鎖失敗
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
}
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
/**
就是說如果其他線程已經獲取了寫鎖,並且獲取寫鎖的線程不是當前線程,那麼讀鎖就會獲取失敗
這句話說明當前線程獲取寫鎖之後仍然能夠獲取讀鎖
*/
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
//獲取鎖失敗
return -1;
int r = sharedCount(c);
//讀鎖不該阻塞(如果老二是寫鎖,讀鎖該阻塞) && 讀鎖被獲取的次數小於讀鎖計數 && 嘗試加鎖成功
if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)){
//獲取鎖成功
return 1;
}
//上面CAS獲取讀鎖失敗後,嘗試循環獲取
return fullTryAcquireShared(current);
}
3、readerShouldBlock()方法
這個方法對於公平鎖和非公平鎖的實現是不同的,也就導致了ReentrantReadWriteLock()對於公平和非公平的兩種不同實現:
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract boolean readerShouldBlock();
abstract boolean writerShouldBlock();
}
對於readerShouldBlock()這個抽象方法,公平鎖和非公平鎖都有實現,具體實現有所不同。
對於非公平鎖:
static final class NonfairSync extends Sync {
//...
final boolean readerShouldBlock() {
/**
看 AQS 隊列中第一個節點是否是寫鎖,true 則該阻塞, false 則不阻塞:
由於非公平的競爭,並且讀鎖可以共享,所以可能會出現源源不斷的讀,使得寫鎖永遠競爭不到,然後出現餓死的現象
通過這個策略,當一個寫鎖出現在頭結點後面的時候,會立刻阻塞所有還未獲取讀鎖的其他線程,讓步給寫線程先執行
*/
return apparentlyFirstQueuedIsExclusive();
}
}
對於公平鎖:
static final class FairSync extends Sync {
//...
final boolean readerShouldBlock() {
//對於公平鎖來說,如果有前驅(也就是非頭結點),都會進行等待,不允許競爭鎖
return hasQueuedPredecessors();
}
}
如果獲取讀鎖獲取失敗,就會繼續執行下面的doAcquireShared(arg)方法:想象成acquireQueued()方法
4、doAcquireShared(arg)方法
如果t2線程獲取鎖失敗,這時會進入doAcquireShared(1) 流程,首先也是調用 addWaiter 添加節點,不同之處在於節點被設置爲Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此時 t2 仍處於活躍狀態 。
在該方法中,t2 會看看自己的節點是不是老二,如果是,還會再次調用 tryAcquireShared(1) 來嘗試獲取鎖,如果獲取鎖沒成功,在 doAcquireShared 內 for (;😉 循環一次,把前驅節點的 waitStatus 改爲 -1,再 for (;😉 循環一次嘗試 tryAcquireShared(1) 如果還不成功,那麼在 parkAndCheckInterrupt() 處 park
注意:如果t2線程執行tryAcquireShared(arg)方法獲取鎖失敗,那麼總共會在doAcquireShared(arg)方法中執行3次doAcquireShared(1) 方法獲取鎖,如果還沒有成功,就會進入阻塞狀態
private void doAcquireShared(int arg) {
//將當前線程關聯到一個 Node 對象上, 模式爲共享模式,加入到同步隊列的隊尾
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
//死循環,CAS自旋的方式嘗試獲取鎖
for (;;) {
//獲取當前節點的前驅節點
final Node p = node.predecessor();
//t2 會看看自己的節點是不是老二,如果是,還會再次調用 tryAcquireShared(1) 來嘗試獲取鎖
if (p == head) {
int r = tryAcquireShared(arg);
//如果獲取鎖成功
if (r >= 0) {
//設置新的head節點,並(喚醒 AQS 中下一個 Share 節點)
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//獲取鎖失敗後是否應該被阻塞,如果需要阻塞,就調用 parkAndCheckInterrupt()方法阻塞當前線程
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
2、t3 r.lock, t4 w.lock
這種狀態下,假設又有 t3 加讀鎖和 t4 加寫鎖,這期間 t1 仍然持有鎖,就變成了下面的樣子
3、t1.unlock
1、寫鎖釋放流程及讀鎖加鎖流程
1、release()方法
public static class WriteLock implements Lock, java.io.Serializable {
public void unlock() {
sync.release(1);
}
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public final boolean release(int arg) {
//調用tryrelease()方法嘗試釋放鎖
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//如果頭結點不爲null並且waitStatus!=0 ,喚醒等待隊列中下一個線程unpark()
unparkSuccessor(h);
return true;
}
return false;
}
}
2、tryRelease()方法
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//因爲可重入的原因, 寫鎖計數爲 0, 纔算釋放成功
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
//如果釋放鎖成功,就將加鎖線程設置爲null
setExclusiveOwnerThread(null);
//如果寫鎖計數不爲0,更新寫鎖計數
setState(nextc);
return free;
}
這時會走到寫鎖的
sync.release(1)
流程,調用sync.tryRelease(1)
成功,變成下面的樣子 :
3、unparkSuccessor ()方法
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
//將當前線程的節點狀態置0
compareAndSetWaitStatus(node, ws, 0);
//找到下一個需要喚醒的結點s
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
//如果該節點已經取消獲取鎖,那就從隊尾開始向前找,找到第一個ws<=0的節點,並賦值給s
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//調用unpark()方法,喚醒正在阻塞的線程
if (s != null)
LockSupport.unpark(s.thread);
}
接下來執行喚醒流程
sync.unparkSuccessor
,即讓老二恢復運行:
4、doAcquireShared()方法
這時 t2 在
doAcquireShared
內parkAndCheckInterrupt()
處恢復運行,這回再來一次 for (;😉 執行 tryAcquireShared 成功則讓讀鎖計數加一
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
//2、繼續嘗試獲取鎖資源,讓讀鎖計數加1
int r = tryAcquireShared(arg);
if (r >= 0) {
//3、喚醒下一個線程
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//1、t2線程在這兒被喚醒,就會繼續指向一次for循環
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
5、setHeadAndPropagate (node, 1)方法
這時 t2 已經恢復運行,接下來 t2 調用 setHeadAndPropagate(node, 1),它原本所在節點被置爲頭節點
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);//head指向自己
//如果鎖計數>0,就繼續喚醒下面的線程
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
//檢查下一個節點是否是 shared,如果是將 head 的狀態從 -1 改爲 0 並喚醒老二
if (s == null || s.isShared())
doReleaseShared();
}
}
事情還沒完,在
setHeadAndPropagate
方法內還會檢查下一個節點是否是 shared,如果是則調用
doReleaseShared()
將 head 的狀態從 -1 改爲 0 並喚醒老二,這時 t3 在doAcquireShared
內
parkAndCheckInterrupt()
處恢復運行
這回再來一次 for (;😉 執行 tryAcquireShared 成功則讓讀鎖計數加一
這時 t3 已經恢復運行,接下來 t3 調用 setHeadAndPropagate(node, 1),它原本所在節點被置爲頭節點
下一個節點不是 shared 了,因此不會繼續喚醒 t4 所在節點
4、t2 r.unlock,t3 r.unlock
1、讀鎖釋放流程與寫鎖加鎖流程
1、releaseShared(int arg)方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
2、tryReleaseShared(int unused)方法
t2 進入 sync.releaseShared(1) 中,調用 tryReleaseShared(1) 讓計數減一,但由於計數還不爲零
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//省略不必要的代碼...
//
for (;;) {
int c = getState();
//釋放讀鎖
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
//讀鎖的計數不會影響其它獲取讀鎖線程, 但會影響其它獲取寫鎖線程
//計數爲 0 纔是真正釋放
return nextc == 0;
}
}
3、doReleaseShared()方法
t3 進入 sync.releaseShared(1) 中,調用 tryReleaseShared(1) 讓計數減一,這回計數爲零了,進入
doReleaseShared() 將頭節點從 -1 改爲 0 並喚醒老二,即
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//如果頭結點的waitStatus=Node.SIGNAL,就將其通過CAS改爲0
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//喚醒下一個線程
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
之後 t4 在 acquireQueued 中 parkAndCheckInterrupt 處恢復運行,再次 for (;😉 這次自己是老二,並且沒有其他競爭,tryAcquire(1) 成功,修改頭結點,流程結束
3、鎖降級
由上面的源碼可以看出,線程在獲取讀鎖時,如果state!=0,那麼會先判斷獲取寫鎖的線程是不是當前線程,也就是說一個線程在獲取寫鎖後,還可以獲取讀鎖,當寫鎖釋放後,就降級爲讀鎖了。