基於jdk8分析分析ReentrantReadWriteLock源碼

ReentrantReadWriteLock可重入讀寫鎖,同樣是基於AQS實現的。與ReentrantLock區別就是讀寫分離,將鎖的粒度都將細化,提升性能。在共享數據讀寫操作中,讀操作遠遠超過寫操作的次數,那麼可以理解爲共享數據在大部分時間是不變的。synchronized和ReentrantLock作爲互斥鎖,用於這種場景明顯會降低系統的性能。因此,讀寫分離的重入鎖ReentrantReadWriteLock就出現了。

ReentrantLock特點是:讀-讀並行、讀-寫互斥、寫-寫互斥。存在對共享數據修改操作時,自然需要有互斥鎖的特點,但是在不需要修改數據時,可以認爲是無鎖的並行操作。

來個簡單的例子:


public class TestReentrantReadWriteLock {
    private Map<String, String> cache = new HashMap<>();
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private Lock readLock = readWriteLock.readLock();
    private Lock writeLock = readWriteLock.writeLock();

    public void put(String key, String value) {
        writeLock.lock();
        try {
            cache.put(key, value);
        } finally {
            wirteLock.unlock();
        }
    }
    
    public String get(String key) {
        readLock.lock();
        try {
            return cache.getOrDefault(key, "0");
        } finally {
            readLock.unlock();
        }
    }
}

分析源碼:

ReentrantReadWriteLock實現ReadWriteLock接口

public interface ReadWriteLock {
    //返回讀鎖
    Lock readLock();
    //返回寫鎖
    Lock writeLock();
}

在ReentrantLock中,提供readLock和writeLock的實現方法。構造函數提供支持公平鎖和非公平鎖,默認爲非公平鎖。

    //field
    //ReentrantLock內部類實現的ReadLock、WriteLock
    private final ReentrantReadWriteLock.ReadLock readerLock;
    private final ReentrantReadWriteLock.WriteLock writerLock;
   
    //method
    //返回對應的讀寫鎖
    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

    //構造函數
    public ReentrantReadWriteLock() {
        this(false);
    }
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

 讀鎖(鎖資源共享):

public void lock() {
            //sync是基於AQS實現的公平鎖或非公平鎖
            sync.acquireShared(1);
        }
//AQS提供的獲取讀鎖框架,tryAcquireShared爲具體子類實現的獲取方法
//根據state狀態來識別:鎖是否被持有?是否被讀鎖持有?然後嘗試獲取鎖
    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

         protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
            //爲什麼寫鎖被非當前線程持有時,纔會返回呢?讀-寫不是互斥嗎?
            //假如:某個線程獲取到寫鎖後,業務處理仍然需要讀取某些共享數據時,同線程寫-讀互斥不就完了。
            //這行代碼體現了ReentrantReadWriteLock支持鎖降級特性
            //return -1,則該節點將在AQS獲取共享鎖框架中入隊,如果是head後繼則會再次嘗試
            //調用acquiredShared獲取鎖;非head後繼則嘗試park
            //注意:1、ReentrantReadWriteLock不支持鎖升級(擁有讀鎖申請寫鎖)
            //     2、在鎖升級後,釋放寫鎖,其他線程讀鎖線程就可以獲取讀鎖
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            int r = sharedCount(c);
            //readerShouldBlock()在公平鎖和非公平鎖中提供的策略是不一樣的。
            //非公平鎖中,head後繼節點是EXCLUSIVE類型的,說明存在寫鎖獲取鎖,在讀寫鎖中且非公平鎖條件下,不阻塞讀鎖,會導致寫鎖飢餓,除過這種場景外,其他場景都不會阻塞讀鎖獲取
            //公平鎖中,如果雙向鏈表爲空、(非空&&head後繼==null)、(非空&&head後繼是當前線程)時,return false,可以嘗試獲取鎖資源
            //SHARED_UNIT爲65536,讀寫鎖是將int state屬性值得高16位作爲讀鎖,低16位作爲寫鎖
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                //r==0表示讀鎖爲0,沒有線程獲取讀鎖
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                //r!=0且firstReader==current說明firstReaderHoldCount是自己重入次數
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                //r!=0且firstReader!=current則需要獲取自身的重入次數
                } else {
                    HoldCounter rh = cachedHoldCounter;
                    //rh!=null&&rh.tid跟cachedHoldeCounter不一樣說明上個讀鎖線程不是自己,則通過ThreadLocal獲取自己的重入次數
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    //rh!=null&&rh.tid==current,說明就是自己了
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    //重入次數++
                    rh.count++;
                }
                return 1;
            }
            //當讀鎖判斷後需要阻塞時
            return fullTryAcquireShared(current);
        }
       final int fullTryAcquireShared(Thread current) {
            HoldCounter rh = null;
            for (;;) {
                int c = getState();
                //持有寫鎖的線程不是當前線程,返回-1.後續加入雙向鏈表中
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                //沒有互斥寫鎖,但是需要根據公平或非公平鎖判斷是否需要阻塞
                //1、是公平鎖時,前置節點非當前線程,但是正在執行的線程是當前線程,可以嘗試獲取
                //2、非公平鎖時,當head後繼節點是互斥鎖時,需要阻塞,但是正在執行的線程是當前線程,則可以嘗試獲取鎖。如:寫操作時,需要讀鎖獲取某個值,拒絕讀鎖獲取數據豈不是就死鎖了
                } else if (readerShouldBlock()) {
                    // Make sure we're not acquiring read lock reentrantly
                    if (firstReader == current) {
                        // assert firstReaderHoldCount > 0;
                    } else {
                        //當讀鎖不是第一次獲取讀時,需要嘗試獲取讀鎖。可重入。否則就會容易引起死鎖。而如果是第一次嘗試獲取讀鎖的話,直接退出return -1,進入AQS等待鏈表
                        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");
                //嘗試獲取讀鎖,並return 1
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (sharedCount(c) == 0) {
                        firstReader = current;
                        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;
                }
            }
        }

從上述代碼可以看出,不管是否是公平鎖,有一個問題是例外。如果當前正在執行的已擁有讀鎖或寫鎖的線程再次獲取讀鎖時,都需要循環獲取讀鎖,否則讀鎖一直獲取不到讀鎖加入AQS鏈表中,那麼該線程將會阻塞,嚴重影響性能。

private void doAcquireShared(int arg) {
        //將node節點加入AQS鏈表
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            //線程沒有被阻塞或沒有獲取到鎖,一直循環
            for (;;) {
                //加入到鏈表後,前驅接點是head,說明它是第一個元素,則嘗試獲取鎖
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    //獲取成功,因爲是讀鎖所有需要直接喚醒下一個節點,讓下一個節點也開始獲取鎖
                    //否則就不是讀-讀互斥了
                    if (r >= 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);
        }
    }

讀鎖到這塊差不多結束了,看得頭昏腦漲的……

寫鎖結構跟ReentrantLock完全一樣啊,當然tryAcquire需要子類自己實現了。首先嚐試獲取鎖,獲取失敗進入鏈表,再判斷是否爲head後繼,如果是則再次tryAcquire嘗試獲取鎖,如果不是則看前繼節點是否能安全的park,並檢測中斷屬性值。

//寫鎖結構跟ReentrantLock完全一樣,只需要關注tryAcquire(arg)即可
           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();
            //互斥鎖的狀態
            int w = exclusiveCount(c);
            //state!=0說明,寫鎖前有讀鎖或寫鎖申請了鎖資源
            if (c != 0) {
                //有鎖,w==0說明,上次申請鎖的是讀鎖;current!=線程持有鎖不是當前線程
                //線程持有鎖只會是寫鎖設置的
                if (w == 0 || current != getExclusiveOwnerThread())
                    //獲取失敗時,又到了進入雙向鏈表,是否爲head的後繼嘗試獲取鎖的流程
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //w!=0&&current==getExclusveOwnerThread就是寫鎖重入了
                setState(c + acquires);
                return true;
            }
            //此處state==0,沒有任何線程持有鎖
            //writerShouldBlock是公平鎖和非公平鎖都實現的方法
            //非公平鎖直接返回false,公平鎖則判斷空鏈表或head後繼是當前線程返回false;
            //cas設置state值
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            //設置當前線程獨佔鎖
            setExclusiveOwnerThread(current);
            return true;
        }

Condition:

只有寫鎖支持Condition,讀鎖會拋出UnsupportedOperationException異常,讀鎖不支持Condition的原因,個人理解跟讀寫鎖的應用場景有關,支持高併發的讀。讀之間都要支持Condition的話,讀寫鎖實現會非常複雜,而且又丟了讀寫鎖的應用場景。

讀鎖和寫鎖釋放鎖的邏輯就不細讀了,大致就是:一直嘗試釋放鎖,然後喚醒head後繼節點,接着讓它嘗試獲取鎖的過程。

 

未完待續,去玩會再說……

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