Basic Of Concurrency(十五: Java中的讀寫鎖)

讀寫鎖是一個比前文Java中的鎖更加複雜的鎖.想象一下當你有一個應用須要對資源進行讀寫,然而對資源的讀取次數遠大於寫入.當有兩個線程對同一個資源進行讀取時並不會有併發問題,因此多個線程能夠在同一個時間點安全的讀取資源.可是當一個線程須要對資源進行寫入時,則其餘線程的讀寫都不能同時進行.容許多個讀線程同時操做但只容許一個寫線程寫入資源,這種狀況咱們能夠經過讀寫鎖來解決.html

Java5中的java.util.concurrent包中已有讀寫鎖的實現.但咱們仍是頗有必要知道它的底層遠離.java

Java中讀寫鎖的實現

首先讓咱們理清讀寫資源時所須要的條件:安全

讀操做 若是沒有線程正在進行寫操做而且沒有線程請求進行寫操做.併發

寫操做 若是沒有線程正在進行寫操做和讀操做post

只有沒有線程正在寫入資源或是請求寫入資源時,線程就能夠進行讀取操做.若是咱們確認寫操做比讀操做重要的多,咱們須要升級寫操做的優先級,若是咱們沒有這麼作,那麼在讀操做太多頻繁時候,可能會出現飢餓現象.線程請求寫入操做會一直阻塞到全部的讀取線程釋放讀寫鎖爲止.若是新進行的讀線程一直搶佔先機,那麼寫線程可能會無限期的等待下去.結果就是產生飢餓現象.因此一個線程只能在沒有線程進行寫操做或是請求進行寫操做時才能進行讀取操做.spa

寫線程只有在沒有線程進行讀寫操做是才能進行.除非你想確保線程寫入請求的公平性,否則你能夠忽略有多少線程進行寫操做請求和它們的順序.線程

基於以上給出的限制條件,咱們能夠實現一個公平鎖,以下所示:code

public class ReadWriteLock {
    private int readers = 0;
    private int writers = 0;
    private int writeRequests = 0;
    
    public synchronized void lockRead() throws InterruptedException {
        while(writers > 0 || writeRequests > 0){
            wait();
        }
        readers++;
    }
    
    public synchronized void unlockRead(){
        readers--;
        notifyAll();
    }
    
    public synchronized void lockWrite() throws InterruptedException {
        writeRequests++;
        while (readers > 0 || writers> 0){
            wait();
        }
        writeRequests--;
        writers++;
    }
    
    public synchronized void unlockWrite(){
        writers--;
        notifyAll();
    }
}
複製代碼

ReadWrite對象中,一共有兩個lock()方法和兩個unlock方法,一對讀操做的lock()和unlock()方法以及一對寫操做的lock()和unlock()方法.htm

對於讀操做的限制在lockRead()方法中實現.讀操做只有在一個寫線程取得讀寫鎖或是有一到多個寫請求的狀況下才會阻塞等待.對象

對於寫操做的限制在lockWrite()方法中實現.一個線程想要進行寫操做首先要進行寫請求(writeRequests++).而後纔去檢查是否已經有讀或者寫線程取得讀寫鎖.若沒有則取得讀寫所進行寫入操做,若有則進入while循環內部調用wait()方法進入等待狀態.這個時候當前有多少個寫入請求對本次操做沒有任何影響.

咱們能夠注意到,跟以往不一樣的是,咱們在unlockRead()和unlockWrite()方法中用notifyAll()來代替notify().咱們能夠想象一下下面這種狀況:

在讀寫鎖中同時有讀線程和寫線程在等待中.若是經過notify()喚醒的線程是讀線程時,它會立刻從新進入等待狀態.由於已經有一個寫線程在等待中了,即已經存在一個寫請求了.然而在沒有任何寫線程被喚醒狀況下,將不會發生任何事情.若是換成調用notifyAll()的話,會喚醒全部等待中的線程,不管讀寫.而不是一個一個喚醒.

調用notifyAll()還有一個優點,即同時存在多個讀線程的狀況下,若是unlockWrite()被調用,全部讀線程都可以被同時喚醒和同時操做,不用一個個來.

可重入的讀寫鎖

上文中給出的ReadWrite.class示例並不支持可重入.若是一個線程屢次發起寫入請求,即屢次嘗試獲取讀寫鎖,將會陷入阻塞,由於此前已經有一個寫線程獲取到讀寫鎖了,那就是它本身.可重入須要考慮如下幾種狀況:

  1. 線程1得到讀權限
  2. 線程2請求進行寫操做,但進入阻塞,由於當前有一個讀線程正在進行中
  3. 線程1再次發起讀請求(嘗試再次獲取讀寫鎖),此次線程1陷入阻塞,由於當前已有一個寫請求存在.

這種狀況上文給出的ReadWriteLock.class將會陷入跟死鎖相似的境地.不管線程讀寫都會陷入阻塞.

爲了讓ReadWriteLock.class支持可重入,須要作出一點更改.讀寫操做的可重入性須要分開處理.

讀操做的可重入性

爲了讓ReadWiriteLock支持讀操做的可重入性,咱們須要補充下面限制:

  • 一個讀線程可以屢次進行讀操做,在它已經得到讀權限的狀況下.

爲了確認一個讀線程得到多少次讀寫鎖,咱們須要一個Map引用來記錄每個讀線程獲取到讀寫鎖的次數.當決定調用線程允不容許得到讀寫鎖時須要檢查Map引用中是否存在該線程.對lockRead()和unlockRead()的改寫以下:

public class ReadWriteLock {
    private int writers = 0;
    private int writeRequests = 0;
    private Map<Thread, Integer> readingThreads = new HashMap<>();

    public synchronized void lockRead() throws InterruptedException {
        Thread callingThread = Thread.currentThread();
        while (!canGrantReadAccess(callingThread)) {
            wait();
        }
        readingThreads.put(callingThread, getReadAccessCount(callingThread) + 1);
    }

    public synchronized void unlockRead() {
        Thread callingThread = Thread.currentThread();
        int accessCount = getReadAccessCount(callingThread);
        if (accessCount > 1) {
            readingThreads.put(callingThread, (accessCount - 1));
        } else {
            readingThreads.remove(callingThread);
        }
        notifyAll();
    }

    private boolean canGrantReadAccess(Thread callingThread) {
        if (writers > 0) return false;
        if (isReader(callingThread)) return true;
        return writeRequests == 0;
    }

    private boolean isReader(Thread callingThread) {
        return readingThreads.get(callingThread) != null;
    }

    private int getReadAccessCount(Thread callingThread) {
        int accessCount = 0;
        if (readingThreads.containsKey(callingThread)) {
            accessCount = readingThreads.get(callingThread) + 1;
        }
        return accessCount;
    }
}
複製代碼

你能夠看到,在沒有線程對資源進行寫入操做時,讀線程能夠屢次獲取到讀取權限.更多的,當調用線程已經得到過讀操做時將優先其餘寫入請求獲取到讀寫鎖.

寫操做的可重入性

寫操做只有在已經得到寫權限的狀況下才能屢次獲取讀寫鎖.對lockWrite()和unlockWrite()的改寫以下:

public class ReadWriteLock {
    private int writerAccess = 0;
    private int writeRequests = 0;
    private Map<Thread, Integer> readingThreads = new HashMap<>();
    private Thread writingThread = null;

    public synchronized void lockWrite() throws InterruptedException {
        writeRequests++;
        Thread callingThread = Thread.currentThread();
        while (!canGrantWriteAccess(callingThread)) {
            wait();
        }
        writeRequests--;
        writerAccess++;
        writingThread = callingThread;
    }

    public synchronized void unlockWrite() {
        writerAccess--;
        if(writerAccess == 0) {
            writingThread = null;
        }
        notifyAll();
    }

    private boolean canGrantWriteAccess(Thread callingThread) {
        if (hasReaders()) return false;
        if (writingThread == null) return true;
        return isWriter(callingThread);
    }

    private boolean hasReaders() {
        return readingThreads.size() > 0;
    }

    private boolean isWriter(Thread callingThread) {
        return writingThread == callingThread;
    }
}
複製代碼

咱們須要將當前取得讀寫鎖的讀線程引用起來,以便決定當前調用線程是否已經得到過寫權限,容許屢次獲取讀寫鎖.

寫操做到讀操做的可重入性

有時候一個線程寫操做完成後須要進行讀操做.咱們容許一個已經得到寫權限的線程得到讀權限.同一個線程同時進行讀寫並不會有併發問題.咱們須要對canGrantReadAccess()進行以下改寫:

private boolean canGrantReadAccess(Thread callingThread) {
        if(isWriter(callingThread)) return true;
        if (writers > 0) return false;
        if (isReader(callingThread)) return true;
        return writeRequests == 0;
    }
複製代碼

一個完整的讀寫鎖實現

下面是一個完整的讀寫鎖實例.

public class ReadWriteLock {
    private int writerAccess = 0;
    private int writeRequests = 0;
    private Map<Thread, Integer> readingThreads = new HashMap<>();
    private Thread writingThread = null;

    public synchronized void lockRead() throws InterruptedException {
        Thread callingThread = Thread.currentThread();
        while (!canGrantReadAccess(callingThread)) {
            wait();
        }
        readingThreads.put(callingThread, getReadAccessCount(callingThread) + 1);
    }

    public synchronized void unlockRead() {
        Thread callingThread = Thread.currentThread();
        int accessCount = getReadAccessCount(callingThread);
        if (accessCount > 1) {
            readingThreads.put(callingThread, (accessCount - 1));
        } else {
            readingThreads.remove(callingThread);
        }
        notifyAll();
    }

    private boolean canGrantReadAccess(Thread callingThread) {
        if(isWriter(callingThread)) return true;
        if (writerAccess > 0) return false;
        if (isReader(callingThread)) return true;
        return writeRequests == 0;
    }

    private boolean isReader(Thread callingThread) {
        return readingThreads.get(callingThread) != null;
    }

    private int getReadAccessCount(Thread callingThread) {
        int accessCount = 0;
        if (readingThreads.containsKey(callingThread)) {
            accessCount = readingThreads.get(callingThread) + 1;
        }
        return accessCount;
    }

    public synchronized void lockWrite() throws InterruptedException {
        writeRequests++;
        Thread callingThread = Thread.currentThread();
        while (!canGrantWriteAccess(callingThread)) {
            wait();
        }
        writeRequests--;
        writerAccess++;
        writingThread = callingThread;
    }

    public synchronized void unlockWrite() {
        writerAccess--;
        if(writerAccess == 0) {
            writingThread = null;
            notifyAll();
        }
    }

    private boolean canGrantWriteAccess(Thread callingThread) {
        if (hasReaders()) return false;
        if (writingThread == null) return true;
        return isWriter(callingThread);
    }

    private boolean hasReaders() {
        return readingThreads.size() > 0;
    }

    private boolean isWriter(Thread callingThread) {
        return writingThread == callingThread;
    }
}
複製代碼

在finally語句中調用unlock()

當使用ReadWriteLock來保證臨界區代碼的同步時,臨界區中的代碼可能會拋出異常。因此頗有必要在finally語句中來調用unlockRead()和unlockWrite()方法。不管線程執行的代碼異常與否,始終可以釋放它所持有的讀寫鎖,以讓其餘線程能正常執行。以下所示:

lock.lockWrite();
        try {
            //do critical section code, which may throw exception
        } finally {
            lock.unlockWrite();
        }
複製代碼

咱們使用這個小結構來保證即便臨界區代碼拋出異常,線程也能正常釋放掉所持有的讀寫鎖。若是咱們沒有這麼作,一旦臨界區代碼出現異常,線程將沒法釋放讀寫鎖,以致於其餘調用了該讀寫鎖的lockRead()和lockWrite()方法的線程將會永遠等待下去。

該系列博文爲筆者複習基礎所著譯文或理解後的產物,複習原文來自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial

上一篇: Java中的鎖
下一篇: Reentrance Lockout

相關文章
相關標籤/搜索