ReentrantReadWriteLock底層源碼

聲明:本文爲作者原創,如若轉發,請指明轉發地址

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 在doAcquireSharedparkAndCheckInterrupt()處恢復運行,這回再來一次 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,那麼會先判斷獲取寫鎖的線程是不是當前線程,也就是說一個線程在獲取寫鎖後,還可以獲取讀鎖,當寫鎖釋放後,就降級爲讀鎖了。
在這裏插入圖片描述

4、不可以鎖升級

在這裏插入圖片描述

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章