java併發編程StampedLock

前面介紹的ReadWriteLock可以解決多線程同時讀,但只有一個線程能寫的問題。
如果我們深入分析ReadWriteLock,會發現它有個潛在的問題:如果有線程正在讀,寫線程需要等待讀線程釋放鎖後才能獲取寫鎖,即讀的過程中不允許寫,這是一種悲觀的讀鎖。
要進一步提升併發執行效率,Java 8引入了新的讀寫鎖:StampedLock。

StampedLock和ReadWriteLock相比,改進之處在於:讀的過程中也允許獲取寫鎖後寫入!這樣一來,我們讀的數據就可能不一致,所以,需要一點額外的代碼來判斷讀的過程中是否有寫入,這種讀鎖是一種樂觀鎖。

StampedLock三種模式
悲觀鎖:悲觀鎖則是讀的過程中拒絕有寫入,也就是寫入必須等待。
寫鎖:與讀寫鎖的寫鎖類似,寫鎖和悲觀讀是互斥的。
樂觀鎖:樂觀地估計讀的過程中大概率不會有寫入,因此被稱爲樂觀鎖。

我們來看一個 悲觀鎖 + 寫鎖的 示例:

public class StampedLockTest {

    private static StampedLock lock = new StampedLock();

    //緩存中存儲的數據
    private static Map<String,String> cacheMap = new HashMap<String, String>();
    //模擬數據庫存儲的數據
    private static Map<String,String> dbMap = new HashMap<String, String>();
    static {
        dbMap.put("zhangsan","張三");
        dbMap.put("sili","李四");
    }

    private static String getInfo(String name){
        //獲取悲觀讀
        long stamp = lock.readLock();
        System.out.println(Thread.currentThread().getName()+" 獲取了悲觀讀鎖" +" 用戶名:"+name);
        try {
            if("zhangsan".equals(name)){
                System.out.println(Thread.currentThread().getName()+" 休眠中" +" 用戶名:"+name);
                Thread.sleep(3000);
                System.out.println(Thread.currentThread().getName()+" 休眠結束" +" 用戶名:"+name);
            }
            String info = cacheMap.get(name);
            if (null != info) {
                return info;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //釋放悲觀讀
            lock.unlockRead(stamp);
            System.out.println(Thread.currentThread().getName()+" 釋放了悲觀讀鎖" +" 用戶名:"+name);
        }

        //獲取寫鎖
        stamp = lock.writeLock();
        System.out.println(Thread.currentThread().getName()+" 獲取了寫鎖" +" 用戶名:"+name);
        try {
            //判斷一下緩存中是否被插入了數據
            String info = cacheMap.get(name);
            if (null != info) {
                return info;
            }
            //這裏是往數據庫獲取數據
            String infoByDb = dbMap.get(name);
            //講數據插入緩存
            cacheMap.put(name,infoByDb);
        }finally {
            //System.out.println("線程名:"+Thread.currentThread().getName()+" 釋放了寫鎖" +" 用戶名:"+name);
            //釋放寫鎖
            lock.unlockWrite(stamp);
        }
        return null;
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(() ->{
            getInfo("zhangsan");
        });

        Thread t2 = new Thread(() ->{
            getInfo("lisi");
        });

        //線程啓動
        t1.start();
        t2.start();

        //線程同步
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

這個示例,我們打印後可以看到結果是:悲觀讀鎖與寫鎖是互斥的,那這樣的效率不是和讀寫鎖一樣嗎?爲什麼說它比讀寫鎖更快呢?

別急,我們來看它的樂觀鎖。

public class Point {

    private final StampedLock stampedLock = new StampedLock();

    private double x;
    private double y;

    public void move(double deltaX, double deltaY){
        //獲取寫鎖
        long stamp = stampedLock.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        }finally {
            //釋放寫鎖
            stampedLock.unlockWrite(stamp);
        }
    }

    public double distanceFromOrigin() {
        // 獲得一個樂觀讀鎖
        long stamp = stampedLock.tryOptimisticRead();
        double currentX = x;
        double currentY = y;
        try {
            //休眠3秒,目的是爲了讓其他線程修改掉成員變量的值。
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 檢查樂觀讀鎖後是否有其他寫鎖發生
        if (!stampedLock.validate(stamp)) {
            System.out.println("樂觀讀鎖後有其他寫鎖發生");
            //若存在寫鎖發生,則獲取一個悲觀讀鎖
            stamp = stampedLock.readLock();
            try {
                currentX = x;
                currentY = y;
            }finally {
                // 釋放悲觀讀鎖
                stampedLock.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }


    public static void main(String[] args) {
        Point point = new Point();
        Thread t1 = new Thread(() ->{
            double result = point.distanceFromOrigin();
            System.out.println("result==="+result);
        });
        t1.start();
        try {
            //休眠1秒,目的爲了讓線程t1能執行到獲取成員變量之後
            Thread.sleep(1000);
            //point.move(20,30);
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("執行完畢");
    }
}

和ReadWriteLock相比,寫入的加鎖是完全一樣的,不同的是讀取。注意到首先我們通過tryOptimisticRead()獲取一個樂觀讀鎖,並返回版本號。接着進行讀取,讀取完成後,我們通過validate()去驗證版本號,如果在讀取過程中沒有寫入,版本號不變,驗證成功,我們就可以放心地繼續後續操作。如果在讀取過程中有寫入,版本號會發生變化,驗證將失敗。在失敗的時候,我們再通過獲取悲觀讀鎖再次讀取。由於寫入的概率不高,程序在絕大部分情況下可以通過樂觀讀鎖獲取數據,極少數情況下使用悲觀讀鎖獲取數據。

可見,StampedLock把讀鎖細分爲樂觀讀和悲觀讀,能進一步提升併發效率。但這也是有代價的:一是代碼更加複雜,二是StampedLock是不可重入鎖,不能在一個線程中反覆獲取同一個鎖。

使用StampedLock的注意事項
1.StampedLock屬於ReadWriteLock的子類,ReentrantReadWriteLock也是屬於ReadWriteLock的子類。StampedLock是不可重入鎖。
2.它適用於讀多寫少的情況,如果不是這中情況,請慎用,性能可能還不如synchronized。
3.StampedLock的悲觀讀鎖、寫鎖不支持條件變量。
4.千萬不能中斷阻塞的悲觀讀鎖或寫鎖,如果調用阻塞線程的interrupt(),會導致cpu飆升,如果希望StampedLock支持中斷操作,請使用readLockInterruptibly(悲觀讀鎖)與writeLockInterruptibly(寫鎖)。

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