多線程基礎學習十四:ReadWriteLock與ReentrantReadWriteLock

前面學習的synchronized、lock與ReentrantLock都是獨佔鎖(有的也稱爲互斥鎖),瞭解鎖的概念的時候,有一種鎖叫共享鎖,今天就學習一下共享鎖向光的接口和實現類。

ReadWriteLock的瞭解

在J.U.C(java.util.concurrent)中提供了一種共享鎖的實現,也就是讀寫鎖。
ps:
我對讀寫鎖這個詞印象頗深,剛開始工作的時候,當時處於對併發完全不知道的狀態,面試官問我什麼是讀寫鎖,結果整個人都懵了,啥東西,我們說的是同一種編程語言麼。。。。

讀寫鎖實際上是爲了解決一種特殊場景問題而提出一種概念。舉例說明:

有一個氣候發佈系統,共有三十個不同類別的氣候展示面板,這三十個面板展示的數據都是來自氣候傳感器,有一個線程每隔一段時間從傳感器讀取數據,更新到對應的變量上,而三十個面板的線程隨時都可能讀取這些變量的值,並且要求讀到的值是一樣的。

三十個面板線程對應讀鎖,數據刷新線程對應寫鎖。

這種場景要求:
1、讀取的線程讀取到的數據都是最新的;
2、寫數據的線程寫數據的時候,讀線程不能讀取數據;

在java源碼中,讀寫鎖的頂級接口裏面只提供了兩個基本方法:

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

但是正如源碼註釋裏面所說,雖然看起來很簡單,但是實現類仍然需要注意幾個方面:
1、寫鎖優先級高於讀鎖
2、重入的問題;
3、寫鎖降級問題和讀鎖升級問題;

另外讀寫鎖的性能並不一定優於獨佔鎖,性能受限與讀寫的頻率和操作時間。源碼是這麼說的:

Whether or not a read-write lock will improve performance over the use  of a mutual exclusion lock depends on the frequency that the data is  read compared to being modified, the duration of the read and write  operations, and the contention for the data - that is, the number of  threads that will try to read or write the data at the same time.  For example, a collection that is initially populated with data and  thereafter infrequently modified, while being frequently searched  (such as a directory of some kind) is an ideal candidate for the use of  a read-write lock. However, if updates become frequent then the data  spends most of its time being exclusively locked and there is little, if any  increase in concurrency. Further, if the read operations are too short  the overhead of the read-write lock implementation (which is inherently  more complex than a mutual exclusion lock) can dominate the execution  cost, particularly as many read-write lock implementations still serialize  all threads through a small section of code. Ultimately, only profiling  and measurement will establish whether the use of a read-write lock is  suitable for your application.
在使用互斥鎖時, 讀寫鎖定是否會提高性能取決於讀取數據的頻率與被修改的次數、讀寫操作的持續時間以及數據的爭用, 這是, 將嘗試同時讀取或寫入數據的線程數。 例如, 最初用數據填充的集合, 然後在頻繁地搜索 (如某種目錄) 的情況下不經常修改, 是使用讀寫鎖的理想候選對象。但是, 如果更新變得頻繁, 那麼數據的大部分時間都是完全鎖定的, 並且在併發性方面的增加很少。此外, 如果讀取操作太短, 讀寫鎖實現的開銷 (本質上比互斥鎖更復雜) 可以控制執行成本, 特別是許多讀寫鎖實現仍然序列化 所有線程通過一小部分代碼。最終, 只有性能分析和度量才能確定是否使用讀寫鎖定適合您的應用程序。

ReadWriteLock的實現ReentrantReadWriteLock

想要理解這個實現,需要理解一下幾個方面:
– 公平鎖與非公平鎖的實現
– 讀寫鎖的實現
– 讀寫鎖的使用
– 鎖的升級與降級

其中公平鎖與非公平鎖的實現和ReentrantLock是一樣的,不需要再詳細瞭解。

讀寫鎖的實現

在之前額學習中瞭解到,lock實現類中的同步是通過AQS同步器來實現的,在ReentrantReadWriteLock也是通過同步器實現的,只不過讀鎖調用的是同步器的獲取共享鎖方法,寫鎖調用的是獨佔鎖方法。

讀鎖的實現:

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

    public void lock() {
            sync.acquireShared(1);
        }

    public void lockInterruptibly() throws InterruptedException {
            sync.acquireSharedInterruptibly(1);
        }
        ........
}

實際上就是通過同步器調用獲取共享鎖方法。

寫鎖的實現:

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

    public void lock() {
            sync.acquire(1);
        }

    public void lockInterruptibly() throws InterruptedException {
            sync.acquireInterruptibly(1);
        }
  .........
}

寫鎖獲取的都是獨佔鎖。

既然知道讀寫鎖的實現,那同步器是誰?

讀鎖的構造方法:

protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }

寫鎖的構造方法:

protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }

讀寫鎖的初始化:

 public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

fair參數就不用說了(是否是公平鎖),讀寫鎖初始化的參數是this,也就是當前對象,當前對象是ReentrantReadWriteLock,也就是說讀寫鎖使用的AQS都是ReentrantReadWriteLock的同步器,讀鎖使用其中共享鎖方法,寫鎖使用獨佔鎖方法。

讀寫鎖的使用

常用寫法:

public class ReadWriteLockTest {


    public static void main (String[] args) {

        Test test = new Test();

        new WriteThread(test).start(); // 寫數據

        for (int i = 0; i < 20; i++) { //讀取線程
            new ReadThread(test).start();
        }

        new WriteThread(test).start(); // 寫數據
    }


    static class WriteThread extends  Thread {

        private Test test;

        public WriteThread (Test test) {
            this.test = test;
        }


        @Override
        public void run () {
            test.add();
        }
    }


    static class ReadThread extends  Thread {

        private Test test;

        public ReadThread (Test test) {
            this.test = test;
        }


        @Override
        public void run () {
            test.getNum();
        }
    }


    static  class Test {

        private int num;
        private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
        private ReentrantReadWriteLock.ReadLock readLock = rwl.readLock();
        private ReentrantReadWriteLock.WriteLock writeLock = rwl.writeLock();

        private int getNum() {

            readLock.lock();
            try {
                System.out.println("讀數據==" + num);
                return  num;
            } finally {
                readLock.unlock();
            }
        }

        private void add() {
            writeLock.lock();
            try {
                num++;
                System.out.println("寫數據==" + num);
            } finally {
                writeLock.unlock();
            }
        }

    }
}

輸出結果:

讀數據==0
讀數據==0
讀數據==0
讀數據==0
讀數據==0
寫數據==1
讀數據==1
讀數據==1
讀數據==1
讀數據==1
讀數據==1
讀數據==1
讀數據==1
讀數據==1
讀數據==1
讀數據==1
讀數據==1
讀數據==1
讀數據==1
讀數據==1
讀數據==1
寫數據==2

在大多數使用讀寫鎖的時候,都是很多線程去讀,少量的寫線程寫數據。這種寫法中沒有涉及降級升級的問題。

鎖的升級與降級

之前學習同步的相關知識中都沒有設計的升級降級的問題。
在讀寫鎖中存在兩種鎖,讀鎖和寫鎖,兩種鎖的併發級別是不一樣的,寫鎖的併發級別高於讀鎖,讀寫鎖有兩個特徵:

  1. 寫鎖會阻塞其它的寫鎖和所有的讀鎖
  2. 讀鎖會阻塞寫鎖,不會阻塞讀鎖

所謂降級:寫鎖降級爲讀鎖;
所謂升級:讀鎖升級爲寫鎖;

首先一點,在ReentrantReadWritreLock中是不支持鎖升級的,至於爲什麼,我不理解,網上很多人都說不支持,所以這裏就主要學習一下鎖降級的問題。

這是根據網上的講解寫的一個例子:

public class DowngradeTest {

    private  boolean  valid = false;
    private int num = 0;
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock readLock = rwl.readLock();
    private ReentrantReadWriteLock.WriteLock writeLock = rwl.writeLock();


    public static void main (String[] args) {
        DowngradeTest test = new DowngradeTest();
        new DowngradeThread(test).start();

        SleepUtil.sleep(1000);

        new WriteThread(test).start();
    }

    static class WriteThread extends Thread {
        private DowngradeTest test;


        public WriteThread (DowngradeTest test) {
            this.test = test;
        }

        @Override
        public void run () {
            test.changeData();
        }
    }


    static class DowngradeThread extends Thread {

        private DowngradeTest test;

        public DowngradeThread (DowngradeTest test) {
            this.test = test;
        }

        @Override
        public void run () {
            test.downgrade();
        }
    }

    private void downgrade() {

        while (true) {
            readLock.lock();            
            if (valid) {
                readLock.unlock();
                writeLock.lock();
                System.out.println("寫鎖第二次獲取鎖");
                num++;
                valid = false;
                readLock.lock();
                writeLock.unlock();
                System.out.println("修改後的num===" + num);
                System.out.println("修改後的valid====" + valid);
                SleepUtil.sleep(500);
            }
            readLock.unlock();
        }
    }

    private void changeData() {
        writeLock.lock();

        try {
            System.out.println("寫鎖第一次修改數據");
            valid = true;
            num++;
        } finally {
            writeLock.unlock();
        }
    }
}

輸出結果:

寫鎖第一次修改數據
寫鎖第二次獲取鎖
修改後的num===2
修改後的valid====false
readLock.lock();
writeLock.unlock();

網上說這兩句代碼的順序不能錯,否則就可能出現錯誤數據,我解釋不了,但是<

      System.out.println("修改後的num===" + num);
      System.out.println("修改後的valid====" + valid);

在我看來,所謂升級降級,只不過是在讀線程中使用了寫鎖,在寫線程中使用了讀鎖。

總結

讀寫鎖的特徵:

  1. 寫鎖的併發級別高於讀鎖
  2. 讀鎖是共享鎖,寫鎖是獨佔鎖
  3. ReentrantReadWriteLock中寫鎖可以降級爲讀鎖,讀鎖不能升級爲寫鎖
  4. ReentrantReadWriteLock支持公平鎖和非公平鎖
發佈了73 篇原創文章 · 獲贊 13 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章