ReentrantReadWriteLock讀寫鎖及其在 RxCache 中的使用

Tony沈哲發表於2019-02-08

cool girl.jpg

一. ReentrantReadWriteLock讀寫鎖

Lock 是相當於 synchronized 更物件導向的同步方式,ReentrantLock 是 Lock 的實現。

本文要介紹的 ReentrantReadWriteLock 跟 ReentrantLock 並沒有直接的關係,因為它們之間沒有繼承和實現的關係。

但是 ReentrantReadWriteLock 擁有讀鎖(ReadLock)和寫鎖(WriteLock),它們分別都實現了 Lock。

    /** Inner class providing readlock */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;
複製程式碼

ReentrantReadWriteLock 在使用讀鎖時,其他執行緒可以進行讀操作,但不可進行寫操作。ReentrantReadWriteLock 在使用寫鎖時,其他執行緒讀、寫操作都不可以。ReentrantReadWriteLock 能夠兼顧資料操作的原子性和讀寫的效能。

1.1 公平鎖和非公平鎖

從 ReentrantReadWriteLock 的建構函式中可以看出,它預設使用了非公平鎖。

    /**
     * Creates a new {@code ReentrantReadWriteLock} with
     * default (nonfair) ordering properties.
     */
    public ReentrantReadWriteLock() {
        this(false);
    }

    /**
     * Creates a new {@code ReentrantReadWriteLock} with
     * the given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
複製程式碼

在 Java 中所謂公平鎖是指,每個執行緒在獲取鎖時,會先檢視此鎖維護的等待佇列,如果為佇列空或者當前執行緒執行緒是等待佇列的第一個,則佔有鎖。否則就會加入到等待佇列中,以後按照 FIFO 的順序從佇列中取出。

非公平鎖在獲取鎖時,不會遵循 FIFO 的順序,而是直接嘗試獲取鎖。如果獲取不到鎖,則像公平鎖一樣自動加入到佇列的隊尾等待。

非公平鎖的效能要高於公平鎖。

1.2 讀鎖

讀鎖是一個共享鎖。讀鎖是 ReentrantReadWriteLock 的內部靜態類,它的 lock()、trylock()、unlock() 都是委託 Sync 類實現。

Sync 是真正實現讀寫鎖功能的類,它繼承自 AbstractQueuedSynchronizer 。

1.3 寫鎖

寫鎖是一個排他鎖。寫鎖也是 ReentrantReadWriteLock 的內部靜態類,它的 lock()、trylock()、unlock() 也都是委託 Sync 類實現。寫鎖的程式碼類似於讀鎖,但是在同一時刻寫鎖是不能被多個執行緒所獲取,它是獨佔式鎖。

寫鎖可以降級成讀鎖,下面會介紹鎖降級。

1.4 鎖降級

鎖降級是指先獲取寫鎖,再獲取讀鎖,然後再釋放寫鎖的過程 。鎖降級是為了保證資料的可見性。鎖降級是 ReentrantReadWriteLock 重要特性之一。

值得注意的是,ReentrantReadWriteLock 並不能實現鎖升級。

二. RxCache 中使用讀寫鎖

RxCache 是一款支援 Java 和 Android 的 Local Cache 。目前,支援堆記憶體、堆外記憶體(off-heap memory)、磁碟快取。

github地址:github.com/fengzhizi71…

RxCache 的 CacheRepository 類實現了快取操作的類,它使用了 ReentrantReadWriteLock 用於保證快取在讀寫時避免出現多執行緒的併發問題。

首先,建立一個讀寫鎖,並獲得讀鎖、寫鎖的例項。

class CacheRepository {

    private Memory memory;
    private Persistence persistence;

    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();

    ......
}
複製程式碼

在快取的讀操作時,使用讀鎖。

    boolean containsKey(String key) {

        readLock.lock();

        try {
            if (Preconditions.isBlank(key)) return false;

            return (memory != null && memory.containsKey(key)) || (persistence != null && persistence.containsKey(key));

        } finally {

            readLock.unlock();
        }
    }
複製程式碼

在快取的寫操作時,使用寫鎖。

    void remove(String key) {

        writeLock.lock();

        try {
            if (Preconditions.isNotBlank(key)) {

                if (memory != null) {
                    memory.evict(key);
                }

                if (persistence != null) {
                    persistence.evict(key);
                }
            }

        } finally {

            writeLock.unlock();
        }
    }
複製程式碼

對於某一個方法,如果在讀操作做完之後要進行寫操作,則需要先釋放讀鎖,再獲取寫鎖(否則會死鎖)。寫操作之後,還需要進行讀操作的話,可以使用鎖降級。

    <T> Record<T> get(String key, Type type, CacheStrategy cacheStrategy) {

        readLock.lock();

        try {
            Record<T> record = null;

            if (Preconditions.isNotBlanks(key, type)) {

                switch (cacheStrategy) {

                    case MEMORY: {

                        if (memory!=null) {

                            record = memory.getIfPresent(key);
                        }

                        break;
                    }

                    case PERSISTENCE: {

                        if (persistence!=null) {

                            record = persistence.retrieve(key, type);
                        }

                        break;
                    }

                    case ALL: {

                        if (memory != null) {

                            record = memory.getIfPresent(key);
                        }

                        if (record == null && persistence != null) {

                            record = persistence.retrieve(key, type);

                            if (memory!=null && record!=null && !record.isExpired()) { // 如果 memory 不為空,record 不為空,並且沒有過期

                                readLock.unlock(); // 先釋放讀鎖
                                writeLock.lock();  // 再獲取寫鎖

                                try {
                                    if (record.isNeverExpire()) { // record永不過期的話,直接儲存不需要計算ttl

                                        memory.put(record.getKey(),record.getData());
                                    } else {

                                        long ttl = record.getExpireTime()- (System.currentTimeMillis() - record.getCreateTime());
                                        memory.put(record.getKey(),record.getData(), ttl);
                                    }

                                    readLock.lock();    // 寫鎖在沒有釋放之前,獲得讀鎖 (鎖降級)
                                } finally {

                                    writeLock.unlock(); // 釋放寫鎖
                                }
                            }
                        }
                        break;
                    }
                }
            }

            return record;
        } finally {

            readLock.unlock();
        }
    }
複製程式碼

三. 總結

ReentrantReadWriteLock 讀寫鎖適用於讀多寫少的場景,以提高系統的併發性。因此,RxCache 使用讀寫鎖來實現快取的操作。

RxCache 系列的相關文章:

  1. 堆外記憶體及其在 RxCache 中的使用
  2. Retrofit 風格的 RxCache及其多種快取替換演算法
  3. RxCache 整合 Android 的持久層框架 greenDAO、Room
  4. 給 Java 和 Android 構建一個簡單的響應式Local Cache

Java與Android技術棧:每週更新推送原創技術文章,歡迎掃描下方的公眾號二維碼並關注,期待與您的共同成長和進步。

ReentrantReadWriteLock讀寫鎖及其在 RxCache 中的使用

相關文章