1. 程式人生 > 其它 >JUC學習筆記(七)

JUC學習筆記(七)

現實中有這樣一種場景:對共享資源有讀和寫的操作,且寫操作沒有讀操作那麼頻繁。在沒有寫操作的時候,多個執行緒同時讀一個資源沒有任何問題,所以應該允許多個執行緒同時讀取共享資源;但是如果一個執行緒想去寫這些共享資源,就不應該允許其他執行緒對該資源進行讀和寫的操作了。

1、讀寫鎖

1.1、讀寫鎖介紹

現實中有這樣一種場景:對共享資源有讀和寫的操作,且寫操作沒有讀操作那麼頻繁。在沒有寫操作的時候,多個執行緒同時讀一個資源沒有任何問題,所以應該允許多個執行緒同時讀取共享資源;但是如果一個執行緒想去寫這些共享資源,就不應該允許其他執行緒對該資源進行讀和寫的操作了。
針對這種場景,JAVA的併發包提供了讀寫鎖 ReentrantReadWriteLock, 它表示兩個鎖,一個是讀操作相關的鎖,稱為共享鎖;一個是寫相關的鎖,稱為排他鎖
(1) 執行緒進入讀鎖的前提條件:

  • 沒有其他執行緒的寫鎖
  • 沒有寫請求, 或者有寫請求,但呼叫執行緒和持有鎖的執行緒是同一個(可重入 鎖)。

(2) 執行緒進入寫鎖的前提條件:

  • 沒有其他執行緒的讀鎖
  • 沒有其他執行緒的寫鎖

讀寫鎖具有以下三個重要的特性:
(1)公平選擇性:支援非公平(預設)和公平的鎖獲取方式,吞吐量還是非公平優於公平。
(2)重進入:讀鎖和寫鎖都支援執行緒重進入。
(3)鎖降級:遵循獲取寫鎖、獲取讀鎖再釋放寫鎖的次序,寫鎖能夠降級成為讀鎖。

1.2、ReentrantReadWriteLock

ReentrantReadWriteLock 類的整體結構

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
    /**
     * 讀鎖
     */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /**
     * 寫鎖
     */
    private final ReentrantReadWriteLock.WriteLock writerLock;

    final Sync sync;

    /**
     * 使用預設(非公平)的排序屬性建立一個新的ReentrantReadWriteLock
     */
    public ReentrantReadWriteLock() {
        this(false);
    }

    /**
     * 使用給定的公平策略建立一個新的 ReentrantReadWriteLock
     */
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

    /**
     * 返回用於寫入操作的鎖
     */
    public ReentrantReadWriteLock.WriteLock writeLock() {
        return writerLock;
    }

    /**
     * 返回用於讀取操作的鎖
     */
    public ReentrantReadWriteLock.ReadLock readLock() {
        return readerLock;
    }

    abstract static class Sync extends AbstractQueuedSynchronizer {
    }

    static final class NonfairSync extends Sync {
    }

    static final class FairSync extends Sync {
    }

    public static class ReadLock implements Lock, java.io.Serializable {
    }

    public static class WriteLock implements Lock, java.io.Serializable {
    }
}

可以看到,ReentrantReadWriteLock 實現了 ReadWriteLock 介面,ReadWriteLock 介面定義了獲取讀鎖和寫鎖的規範,具體需要實現類去實現;同時其還實現了 Serializable 介面,表示可以進行序列化,在原始碼中可以看到 ReentrantReadWriteLock 實現了自己的序列化邏輯。

1.3、案例

場景: 使用 ReentrantReadWriteLock 對一個 hashmap 進行讀和寫操作

public class ReadWriteLockDemo {
    public static void main(String[] args) throws InterruptedException {
        MyCache myCache = new MyCache();
        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                myCache.put(num + "", num);
            }, String.valueOf(i)).start();
        }


        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                Object result = myCache.get(num + "");
            }, String.valueOf(i)).start();
        }
    }
}

class MyCache {
    private volatile Map<String, Object> map = new HashMap<>();
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    // 放資料
    public void put(String key, Object value) {

        Lock lock = readWriteLock.writeLock();
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "正在進行寫操作:" + key);
            TimeUnit.MICROSECONDS.sleep(300);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "完成寫操作:" + key);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    // 取資料
    public Object get(String key) {
        Lock lock = readWriteLock.readLock();
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "正在進行讀操作:" + key);
            TimeUnit.MICROSECONDS.sleep(300);
            Object result = map.get(key);
            System.out.println(Thread.currentThread().getName() + "完成讀操作:" + key);
            return result;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return null;
    }
}

1.4、小結

  • 線上程持有讀鎖的情況下,該執行緒不能取得寫鎖(因為獲取寫鎖的時候,如果發 現當前的讀鎖被佔用,就馬上獲取失敗,不管讀鎖是不是被當前執行緒持有)。
  • 線上程持有寫鎖的情況下,該執行緒可以繼續獲取讀鎖(獲取讀鎖時如果發現寫 鎖被佔用,只有寫鎖沒有被當前執行緒佔用的情況才會獲取失敗)。

原因: 當執行緒獲取讀鎖的時候,可能有其他執行緒同時也在持有讀鎖,因此不能把 獲取讀鎖的執行緒“升級”為寫鎖;而對於獲得寫鎖的執行緒,它一定獨佔了讀寫 鎖,因此可以繼續讓它獲取讀鎖,當它同時獲取了寫鎖和讀鎖後,還可以先釋
放寫鎖繼續持有讀鎖,這樣一個寫鎖就“降級”為了讀鎖。