1. 程式人生 > >Android讀寫鎖的應用,以及最佳的磁碟快取設計

Android讀寫鎖的應用,以及最佳的磁碟快取設計

前言

相信磁碟快取在絕大部分的app上都有應用,相對於資料庫快取來說,可以不要注重於快取的管理,比較開放和隨意。 再加上jakewharton早年間釋出的disklrucache框架,讓我們使用磁碟快取更加簡單,效率上和資料庫快取也拉進了一步,以後有時間我在加上disklrucache的快取解讀。

但是在多執行緒的環境下,對同一份資料進行讀寫,會涉及到執行緒安全的問題。比如在一個執行緒讀取資料的時候,另外一個執行緒在寫資料,而導致前後資料的不一致性;一個執行緒在寫資料的時候,另一個執行緒也在寫,同樣也會導致執行緒前後看到的資料的不一致性。更嚴重的是一個執行緒在寫的時候,另一個執行緒在讀。這裡的資料不一致是對於檔案來說的,當檔案裡的資料儲存的json時,殘缺的資料或者不完整的資料無法生成物件,判斷沒有寫好甚至是報錯閃退。

常見解決方案

使用Synchronized同步鎖保護執行緒安全,但是Synchronized存在明顯的一個性能問題就是讀與讀之間互斥,也就是說兩個執行緒的讀操作是順序執行的 下面給大家看下程式碼方便理解

    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                read(Thread.currentThread());
            }
        }).start();

        new Thread(new Runnable
() { @Override public void run() { read(Thread.currentThread()); } }).start(); } public synchronized static void read(Thread thread){ System.out.println("開始執行時間:"+System.currentTimeMillis()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("結束執行時間:"
+System.currentTimeMillis()); } 複製程式碼

我們來看一下執行結果,結論兩個兩個執行緒的讀操作是順序執行的,如果讀的次數多這個太影響效能了

思考

最佳的方案通俗的來講應該是,可以很多人同時讀,但不能同時寫,有人在寫的時候不能同時讀也不能同時寫,官方說法是讀和讀互不影響,讀和寫互斥,寫和寫互斥,好了接下來就是介紹今天的主角ReadWriteLock 讀寫鎖

ReadWriteLock介紹

1.1 ReadWriteLock的位置

ReadWriteLock是Java自帶的 所處位置 java.util.concurrent.locks,屬於java併發方案中的一種

1.2 ReadWriteLock是一個介面,主要有兩個方法,如下

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

複製程式碼

既然只是介面,那我們真正要用的是實現了該介面的類 ReentrantReadWriteLock 可重入讀寫鎖

1.3可重人

可重入鎖,就是說一個執行緒在獲取某個鎖後,還可以繼續獲取該鎖,即允許一個執行緒多次獲取同一個鎖。通俗的來講就是支援在同一個執行緒裡面對多個檔案進行讀寫操作,都可以獲取同一個鎖,但是獲取多少鎖就要回收多少鎖,下面給個例子方便理解

    public static void main(String[] args) {

        final ReadWriteLock lock = new ReentrantReadWriteLock();

        lock.writeLock().lock();
        lock.writeLock().lock();

        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.writeLock().lock();
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("子執行緒執行");
                lock.writeLock().unlock();
            }
        }).start();

        System.out.println("主執行緒執行");
        lock.writeLock().unlock();
//        lock.writeLock().unlock(); 獲取兩次鎖,只釋放一次鎖
        
    }

複製程式碼

執行結果

注意:因為主執行緒2次獲取了鎖,但是卻只釋放1次鎖,造成死鎖,導致新執行緒永遠也不能獲取鎖。一個執行緒獲取多少次鎖,就必須釋放多少次鎖

1.4 獲取鎖順序

  • 非公平模式(預設)

    當以非公平初始化時,讀鎖和寫鎖的獲取的順序是不確定的。非公平鎖主張競爭獲取,可能會延緩一個或多個讀或寫執行緒,但是會比公平鎖有更高的吞吐量。

  • 公平模式

    當以公平模式初始化時,執行緒將會以佇列的順序獲取鎖。噹噹前執行緒釋放鎖後,等待時間最長的寫鎖執行緒就會被分配寫鎖;或者有一組讀執行緒組等待時間比寫執行緒長,那麼這組讀執行緒組將會被分配讀鎖。

  • 原始碼如下

    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);
    }

複製程式碼

1.5 鎖升級和鎖降級

  • 鎖降級:從寫鎖變成讀鎖;
  • 鎖升級:從讀鎖變成寫鎖。
  • ReentrantReadWriteLock 只支援鎖降級
  • 建議儘量不要使用鎖降級操作,獲取什麼鎖就回收什麼鎖,同一執行緒儘量不要使用兩種鎖,最為安全,除非有特殊操作則需注意

2 磁碟快取最佳設計

提供抽象類BaseCache的原始碼,具體實現大家可以通過自己的實際情況去拓展

 public abstract class BaseCache {

    private final ReadWriteLock mLock = new ReentrantReadWriteLock();

    /**
     * 讀取快取
     *
     * @param key       快取key
     * @param existTime 快取時間
     */
    final <T> T load(Type type, String key, long existTime) {
        //1.先檢查key
        Utils.checkNotNull(key, "key == null");

        //2.判斷key是否存在,key不存在去讀快取沒意義
        if (!containsKey(key)) {
            return null;
        }

        //3.判斷是否過期,過期自動清理
        if (isExpiry(key, existTime)) {
            remove(key);
            return null;
        }

        //4.開始真正的讀取快取
        mLock.readLock().lock();
        try {
            // 讀取快取
            return doLoad(type, key);
        } finally {
            mLock.readLock().unlock();
        }
    }

    /**
     * 儲存快取
     *
     * @param key   快取key
     * @param value 快取內容
     * @return
     */
    final <T> boolean save(String key, T value) {
        //1.先檢查key
        Utils.checkNotNull(key, "key == null");

        //2.如果要儲存的值為空,則刪除
        if (value == null) {
            return remove(key);
        }

        //3.寫入快取
        boolean status = false;
        mLock.writeLock().lock();
        try {
            status = doSave(key, value);
        } finally {
            mLock.writeLock().unlock();
        }
        return status;
    }

    /**
     * 刪除快取
     */
    final boolean remove(String key) {
        mLock.writeLock().lock();
        try {
            return doRemove(key);
        } finally {
            mLock.writeLock().unlock();
        }
    }


    /**
     * 獲取快取大小
     * @return
     */
    long size() {
        return getSize();
    }

    /**
     * 清空快取
     */
    final boolean clear() {
        mLock.writeLock().lock();
        try {
            return doClear();
        } finally {
            mLock.writeLock().unlock();
        }
    }

    /**
     * 是否包含 加final 是讓子類不能被重寫,只能使用doContainsKey
     * 這裡加了鎖處理,操作安全。<br>
     *
     * @param key 快取key
     * @return 是否有快取
     */
    public final boolean containsKey(String key) {
        mLock.readLock().lock();
        try {
            return doContainsKey(key);
        } finally {
            mLock.readLock().unlock();
        }
    }

    /**
     * 是否包含  採用protected修飾符  被子類修改
     */
    protected abstract boolean doContainsKey(String key);

    /**
     * 是否過期
     */
    protected abstract boolean isExpiry(String key, long existTime);

    /**
     * 讀取快取
     */
    protected abstract <T> T doLoad(Type type, String key);

    /**
     * 儲存
     */
    protected abstract <T> boolean doSave(String key, T value);

    /**
     * 刪除快取
     */
    protected abstract boolean doRemove(String key);

    /**
     * 清空快取
     */
    protected abstract boolean doClear();

    /**
     * 獲取快取大小
     *
     * @return
     */
    protected abstract long getSize();
}

複製程式碼