JUC之讀寫鎖問題

xbhog發表於2022-01-13

讀寫鎖

讀寫鎖在同一時刻可以允許多個讀執行緒訪問,但是在寫執行緒訪問時,所有的讀執行緒和其他寫執行緒均被阻塞。讀寫鎖維護了一對鎖,一個讀鎖和一個寫鎖,通過分離讀鎖和寫鎖,使得併發性相比一般的排他鎖有了很大提升。

讀操作可以多個執行緒,寫操作只能一個執行緒

Java併發包提供讀寫鎖的實現是 ReentrantReadWriteLock

特性:

  1. 支援公平性和非公平的鎖獲取方式
  2. 支援重進入:以讀寫執行緒為例,當讀執行緒獲取讀鎖以後,還能再次獲取讀鎖,而寫執行緒在獲取寫鎖時還未完全釋放的時候還能再獲取寫鎖以及也能獲取讀鎖。
  3. 鎖降級。寫鎖可以降級為讀鎖,但是讀鎖不能升級為寫鎖

鎖降級的定義:

鎖降級指的是寫鎖降級成為讀鎖。如果當前執行緒擁有寫鎖,然後將其釋放,最後再獲取讀鎖,這種分段完成的過程不能稱之為鎖降級。鎖降級是指把持住(當前擁有的)寫鎖,再獲取到讀鎖,隨後釋放(先前擁有的)寫鎖的過程。

寫鎖可以降級為讀鎖順序:獲取寫鎖----獲取讀鎖------釋放寫鎖------釋放讀鎖。

其缺點:會造成鎖飢餓問題 一直讀,沒有寫操作。

資源與鎖的三個狀態:

  1. 無鎖,多執行緒搶奪資源 亂
  2. 新增鎖(Synchronized和ReentrantLock) 都是獨佔,讀讀、讀寫、寫寫都是獨佔,每次只能一個操作
  3. 讀寫鎖,讀讀可以共享,提升效能,同時可以多人進行讀操作

ReentrantReadWriteLock 目的就是:提高讀操作的吞吐量 (可用於讀多寫少的情況下)

讀寫鎖可重入的理解:

讀鎖的重入是允許多個申請讀操作的執行緒,而寫鎖同時只能允許單個執行緒佔有,該執行緒的寫操作可以重入。

如果一個執行緒佔有了寫鎖,在不釋放寫鎖的情況下,它還能佔有讀鎖,也就是鎖的降級。

如果一個執行緒同時佔有了讀鎖和寫鎖,在完全釋放了寫鎖,那麼就轉換為了讀鎖,以後寫操作無法重入,如果寫鎖未完全釋放時,寫操作時可以重入的。

失敗例子:

package com.RWLock;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

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

    //寫操作
    public void put(String key,Object value) throws InterruptedException {
        System.out.println(Thread.currentThread().getName()+"\t------寫入資料"+key);
        TimeUnit.SECONDS.sleep(1);
        map.put(key,value);
        System.out.println(Thread.currentThread().getName()+"\t------寫入完成"+key);
    }
    //讀操作
    public void get(String key) throws InterruptedException {
        System.out.println(Thread.currentThread().getName()+"\t------讀資料"+key);
        TimeUnit.SECONDS.sleep(1);
        map.get(key);
        System.out.println(Thread.currentThread().getName()+"\t------讀取完成"+key);
    }
}


public class readWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        //多個執行緒進行寫操作
        for (int i = 1; i <= 5; i++) {
            int finalI = i;
            new Thread(()->{
                try {
                    myCache.put(finalI +"", finalI +"");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
        for (int i = 1; i <= 5; i++) {
            int finalI = i;
            new Thread(()->{
                try {
                    myCache.get(finalI+"");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

使用讀寫鎖以後:

Cache組合一個非執行緒安全的HashMap作為快取的實現,同時使用讀寫鎖的讀鎖和寫鎖來保證Cache是執行緒安全的。在讀操作get(String key)方法中,需要獲取讀鎖,這使得併發訪問該方法時不會被阻塞。寫操作put(String key,Object value)方法和clear()方法,在更新HashMap時必須提前獲取寫鎖,當獲取寫鎖後,其他執行緒對於讀鎖和寫鎖的獲取均被阻塞,而只有寫鎖被釋放之後,其他讀寫操作才能繼續。

package com.RWLock;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class MyCache{
    private volatile Map<String,Object> map = new HashMap<String,Object>();
    //可重入的讀寫鎖
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    //寫操作
    public void put(String key,Object value) throws InterruptedException {
        try{
            readWriteLock.writeLock().lock(); //寫鎖
            System.out.println(Thread.currentThread().getName()+"\t------寫入資料"+key);
            TimeUnit.SECONDS.sleep(1);
            map.put(key,value);
            System.out.println(Thread.currentThread().getName()+"\t------寫入完成"+key);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally{
            readWriteLock.writeLock().unlock();
        }

    }
    //讀操作
    public void get(String key) {
        try {
            readWriteLock.readLock().lock();   //讀鎖
            System.out.println(Thread.currentThread().getName()+"\t------讀資料"+key);
            TimeUnit.SECONDS.sleep(1);
            map.get(key);
            System.out.println(Thread.currentThread().getName()+"\t------讀取完成"+key);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally{
            readWriteLock.readLock().unlock();
        }


    }
}


public class readWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        //多個執行緒進行寫操作
        for (int i = 1; i <= 5; i++) {
            int finalI = i;
            new Thread(()->{
                try {
                    myCache.put(finalI +"", finalI +"");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
        for (int i = 1; i <= 5; i++) {
            int finalI = i;
            new Thread(()->{
                try {
                    myCache.get(finalI+"");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

讀寫鎖的設計:依賴於同步器的同步狀態實現的。

同步狀態表示鎖被一個執行緒重複獲取的次數,而讀寫鎖的自定義同步器需要在同步狀態(一個整型變數)上維護多個讀執行緒和一個寫執行緒的狀態,使得該狀態的設計成為讀寫鎖實現的關鍵

如果一個整型變數維護,按位切割,高16位為讀狀態,低16位為寫狀態。

讀寫鎖是如何迅速確定讀和寫各自的狀態呢?答案是通過位運算

寫鎖的獲取和釋放:

寫鎖是一個支援重進入的排他鎖;

  1. 如果當前執行緒獲取了寫鎖,則增加寫狀態,獨佔
  2. 如果當前執行緒(A)再獲取鎖時,讀鎖已經被獲取或者該執行緒不是已經獲取寫鎖的執行緒(個人理解:如果有執行緒獲取了寫鎖,則其他讀寫執行緒的後續訪問均被阻塞),則當前執行緒(A)進入等待狀態。

獲取讀鎖後不能獲取寫鎖,但是獲取寫鎖後可以獲取讀鎖

讀鎖的獲取和釋放

讀鎖是一個支援重進入的共享鎖,它能夠被多個執行緒同時獲取,在沒有其他寫執行緒訪問(或者寫狀態為0)時,讀鎖總會被成功地獲取,而所做的也只是(執行緒安全的)增加讀狀態。

如果其他執行緒已經獲取了寫鎖,則當前執行緒獲取讀鎖失敗,進入等待狀態。如果當前執行緒獲取了寫鎖或者寫鎖未被獲取,則當前執行緒(執行緒安全,依靠CAS保證)增加讀狀態,成功獲取讀鎖

相關文章