JUC之讀寫鎖問題

讀寫鎖

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

讀操作可以多個線程,寫操作只能一個線程

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保證)增加讀狀態,成功獲取讀鎖

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章