欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java并發(fā)之搞懂讀寫鎖

 更新時(shí)間:2021年11月10日 11:15:03   作者:怎能止步于此  
這篇文章主要介紹了Java并發(fā)之讀寫鎖,文中相關(guān)實(shí)例代碼詳細(xì),測試可用,具有一定參考價(jià)值,需要的朋友可以了解下,希望能夠給你帶來幫助

ReentrantReadWriteLock

我們來探討一下java.concurrent.util包下的另一個(gè)鎖,叫做ReentrantReadWriteLock,也叫讀寫鎖。

實(shí)際項(xiàng)目中常常有這樣一種場景:

在這里插入圖片描述

比如有一個(gè)共享資源叫做Some Data,多個(gè)線程去操作Some Data,這個(gè)操作有讀操作也有寫操作,并且是讀多寫少的,那么在沒有寫操作的時(shí)候,多個(gè)線程去讀Some Data是不會(huì)有線程安全問題的,因?yàn)榫€程只是訪問,并沒有修改,不存在競爭,所以這種情況應(yīng)該允許多個(gè)線程同時(shí)讀取Some Data。

但是若某個(gè)瞬間,線程X正在修改Some Data的時(shí)候,那么就不允許其他線程對(duì)Some Data做任何操作,否則就會(huì)有線程安全問題。

那么針對(duì)這種讀多寫少的場景,J.U.C包提供了ReentrantReadWriteLock,它包含了兩個(gè)鎖:

  • ReadLock:讀鎖,也被稱為共享鎖
  • WriteLock:寫鎖,也被稱為排它鎖

下面我們看看,線程如果想獲取讀鎖,需要具備哪些條件:

  • 不能有其他線程的寫鎖沒有寫請(qǐng)求;
  • 或者有寫請(qǐng)求,但調(diào)用線程和持有鎖的線程是同一個(gè)

再來看一下線程獲取寫鎖的條件:

  • 必須沒有其他線程的讀鎖
  • 必須沒有其他線程的寫鎖

這個(gè)比較容易理解,因?yàn)閷戞i是排他的。

來看下面一段代碼:

public class ReentrantReadWriteLockTest {
    private Object data;
    //緩存是否有效
    private volatile boolean cacheValid;
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    public void processCachedData() {
        rwl.readLock().lock();
        //如果緩存無效,更新cache;否則直接使用data
        if (!cacheValid) {
            //獲取寫鎖前必須釋放讀鎖
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            if (!cacheValid) {
                //更新數(shù)據(jù)
                data = new Object();
                cacheValid = true;
            }
            //鎖降級(jí),在釋放寫鎖前獲取讀鎖
            rwl.readLock().lock();
            //釋放寫鎖,依然持有讀鎖
            rwl.writeLock().unlock();
        }
        // 使用緩存
        // ...
        // 釋放讀鎖
        rwl.readLock().unlock();
    }
}

這段代碼演示的是獲取緩存的時(shí)候,判斷緩存是否過期,如果已經(jīng)過期就更新緩存,如果沒有過期就使用緩存。
可以看到我們先創(chuàng)建了一個(gè)讀鎖,判斷如果緩存有效,就可以使用緩存,使用完之后再把讀鎖釋放。如果緩存無效,就更新緩存執(zhí)行寫操作,所以先把讀鎖給釋放掉,然后創(chuàng)建一個(gè)寫鎖,最后更新緩存,更新完緩存后又重新獲取了一個(gè)讀鎖并釋放掉寫鎖。

從這段代碼里可以看出來,一個(gè)線程在拿到寫鎖之后它還可以繼續(xù)獲得一個(gè)讀鎖。

小結(jié)

我們來總結(jié)一下ReentrantReadWriteLock的三個(gè)特性:

  • 公平性

ReentrantReadWriteLock也可以在初始化時(shí)設(shè)置是否公平。

  • 可重入性

讀鎖以及寫鎖也是支持重入的,比如一個(gè)線程拿到寫鎖后,他依然可以繼續(xù)拿寫鎖,同理讀鎖也可以。

  • 鎖降級(jí)

要想實(shí)現(xiàn)鎖降級(jí),只需要先獲得寫鎖,再獲得讀鎖,最后釋放寫鎖,就可以把一個(gè)寫鎖降級(jí)為讀鎖了。但是一個(gè)讀鎖是沒有辦法升級(jí)為寫鎖的。

最后我們來對(duì)比一下ReentrantLock與ReentrantReadWriteLock

  • ReentrantLock:完全互斥
  • ReentrantReadWriteLock:讀鎖共享,寫鎖互斥

因此在讀多寫少的場景下,ReentrantReadWriteLock的性能、吞吐量各方面都會(huì)比ReentrantLock要好很多。但是對(duì)于寫多的場景ReentrantReadWriteLock就不那么明顯了。

StampedLock

上面我們已經(jīng)探討了ReentrantReadWriteLock能夠大幅度提升讀多寫少場景下的性能,StampedLock是在JDK8引入的,可以認(rèn)為這是一個(gè)ReentrantReadWriteLock的增強(qiáng)版。

那么大家想,既然有了ReentrantReadWriteLock,為什么還要搞一個(gè)StampedLock呢?

這是因?yàn)镽eentrantReadWriteLock在一些特定的場景下存在問題。

比如寫線程的“饑餓”問題。
舉個(gè)例子:假設(shè)現(xiàn)在有超級(jí)多的線程在操作ReentrantReadWriteLock,執(zhí)行讀操作的線程超級(jí)多,而執(zhí)行寫操作的線程很少,而如果這個(gè)執(zhí)行寫操作的線程想要拿到寫鎖,而ReentrantReadWriteLock的寫鎖是排他的,要想拿到寫鎖就意味著其他線程不能有讀鎖也不能有寫鎖,所以在讀線程超級(jí)多,寫線程超級(jí)少的情況下就容易造成寫線程饑餓問題,也就是說,執(zhí)行寫操作的線程可能一直搶不到鎖,即使可以把公平性設(shè)置為true,但是這樣又會(huì)導(dǎo)致性能的下降。

那么我們看看StampedLock怎么玩:

首先,所有獲取鎖的方法都會(huì)返回stamp,它是一個(gè)數(shù)字,如果stamp=0說明操作失敗了,其他的值表示操作成功。

其次就是所有獲取鎖的方法,需要用stamp作為參數(shù),參數(shù)的值必須和獲得鎖時(shí)返回的stamp一致。

其中StampedLock提供了三種訪問模式:

  • Writing模式:類似于ReentrantReadWriteLock的寫鎖R
  • eding(悲觀讀模式):類似于ReentrantReadWriteLock的讀鎖。
  • Optimistic reading:樂觀讀模式

悲觀讀模式:在執(zhí)行悲觀讀的過程中,不允許有寫操作

樂觀讀模式:在執(zhí)行樂觀讀的過程中,允許有寫操作

通過介紹我們可以發(fā)現(xiàn),StampedLock中的悲觀讀與樂觀讀和我們操作數(shù)據(jù)庫中的悲觀鎖、樂觀鎖有一定的相似之處。

此外StampedLock還提供了讀鎖和寫鎖相互轉(zhuǎn)換的功能:

我們知道ReentrantReadWriteLock的寫鎖是可以降級(jí)為讀鎖的,但是讀鎖沒辦法升級(jí)為寫鎖,而StampedLock它提供了讀鎖和寫鎖之間互相轉(zhuǎn)換的功能。

最后,StampedLock是不可重入的,這也是和ReentrantReadWriteLock的一個(gè)區(qū)別。

讀過源碼的同學(xué)可能知道,在StampedLock源碼里有一段注釋:

在這里插入圖片描述

我們來看一下這段注釋,他寫的非常經(jīng)典,演示了StampedLock API如何使用。

class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();
    void move(double deltaX, double deltaY) { // an exclusively locked method
      //添加寫鎖
      long stamp = sl.writeLock();
      try {
        x += deltaX;
        y += deltaY;
      } finally {
        //釋放寫鎖
        sl.unlockWrite(stamp);
      }
    }
    double distanceFromOrigin() { // A read-only method
      //獲得一個(gè)樂觀鎖
      long stamp = sl.tryOptimisticRead();
      // 假設(shè)(x,y)=(10,10)
      // 但是這是一個(gè)樂觀讀鎖,(x,y)可能被其他線程修改為(20,20)
      double currentX = x, currentY = y;
      //因此這里要驗(yàn)證獲得樂觀鎖后,有沒有發(fā)生寫操作
      if (!sl.validate(stamp)) {
         stamp = sl.readLock();
         try {
           currentX = x;
           currentY = y;
         } finally {
            sl.unlockRead(stamp);
         }
      }
      return Math.sqrt(currentX  currentX + currentY  currentY);
    }
    void moveIfAtOrigin(double newX, double newY) { // upgrade
      // Could instead start with optimistic, not read mode
      long stamp = sl.readLock();
      try {
        while (x == 0.0 && y == 0.0) {
          long ws = sl.tryConvertToWriteLock(stamp);
          if (ws != 0L) {
            stamp = ws;
            x = newX;
            y = newY;
            break;
          }
          else {
            sl.unlockRead(stamp);
            stamp = sl.writeLock();
          }
        }
      } finally {
        sl.unlock(stamp);
      }
    }
}

在這里插入圖片描述

這個(gè)類有三個(gè)方法,move方法用來移動(dòng)一個(gè)點(diǎn)的坐標(biāo),instanceFromOrigin用來計(jì)算這個(gè)點(diǎn)到原點(diǎn)的距離,moveIfAtOrigin表示當(dāng)這個(gè)點(diǎn)位于原點(diǎn)的時(shí)候用來移動(dòng)這個(gè)點(diǎn)的坐標(biāo)。

我們來分析一下源碼:

move方法是一個(gè)純粹的寫操作,在操作之前添加寫鎖,操作結(jié)束釋放寫鎖;

instanceOrigin首先獲得一個(gè)樂觀鎖,然后開始讀數(shù)據(jù),我們假設(shè)(x,y)=(10,10),但是這是一個(gè)樂觀讀鎖,(x,y)可能被其他線程修改為(20,20),所以他會(huì)驗(yàn)證獲得樂觀鎖后,有沒有發(fā)生寫操作,如果validate結(jié)果為true的話,表示沒有發(fā)生過寫操作,如果發(fā)生過寫操作,那么就會(huì)改用悲觀讀鎖重讀數(shù)據(jù),然后計(jì)算結(jié)果,當(dāng)然最后要把鎖釋放掉。

最后moveIfAtOrigin方法也比較簡單,主要演示了怎么從悲觀讀鎖轉(zhuǎn)換成寫鎖。

小結(jié)

StampedLock主要通過樂觀讀的方式提升性能,同時(shí)也解決了寫線程的饑餓問題,但是有得必有失,我們從示例代碼中不難看出,StampedLock使用起來要比ReentrantReadWriteLock復(fù)雜很多,所以使用者要在性能和復(fù)雜度之間做一個(gè)取舍。

總結(jié)

本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!

相關(guān)文章

  • Spring Boot集成MyBatis的方法

    Spring Boot集成MyBatis的方法

    今天小編就為大家分享一篇關(guān)于Spring Boot集成MyBatis的方法,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧
    2018-12-12
  • 你都理解創(chuàng)建線程池的參數(shù)嗎?

    你都理解創(chuàng)建線程池的參數(shù)嗎?

    這篇文章主要介紹了創(chuàng)建線程池參數(shù),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-04-04
  • Spring中基于XML的面向切面編程(AOP)詳解

    Spring中基于XML的面向切面編程(AOP)詳解

    這篇文章主要詳細(xì)介紹了Spring中基于XML的面向切面編程(AOP),文中通過代碼示例給大家講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下
    2024-04-04
  • SpringBoot使用JPA實(shí)現(xiàn)查詢部分字段

    SpringBoot使用JPA實(shí)現(xiàn)查詢部分字段

    這篇文章主要介紹了SpringBoot使用JPA實(shí)現(xiàn)查詢部分字段方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-08-08
  • JAVA異常處理機(jī)制之throws/throw使用情況

    JAVA異常處理機(jī)制之throws/throw使用情況

    這篇文章主要介紹了JAVA異常處理機(jī)制之throws/throw使用情況的區(qū)別,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-07-07
  • Mybatis-Plus 通用CRUD的詳細(xì)操作

    Mybatis-Plus 通用CRUD的詳細(xì)操作

    這篇文章主要介紹了Mybatis-Plus 通用CRUD的詳細(xì)操作,包括插入操作,更新操作及刪除操作等,針對(duì)每種操作通過實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下
    2021-09-09
  • Spring中的使用@Async異步調(diào)用方法

    Spring中的使用@Async異步調(diào)用方法

    這篇文章主要介紹了Spring中的使用@Async異步調(diào)用方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-11-11
  • Spring事務(wù)捕獲異常后依舊回滾的解決

    Spring事務(wù)捕獲異常后依舊回滾的解決

    本文主要介紹了Spring事務(wù)捕獲異常后依舊回滾的解決,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-01-01
  • Java concurrency之公平鎖(一)_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理

    Java concurrency之公平鎖(一)_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理

    這篇文章主要為大家詳細(xì)介紹了Java concurrency之公平鎖的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-06-06
  • KafkaListener注解的實(shí)現(xiàn)機(jī)制源碼解析

    KafkaListener注解的實(shí)現(xiàn)機(jī)制源碼解析

    這篇文章主要為大家介紹了KafkaListener注解的實(shí)現(xiàn)機(jī)制源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-10-10

最新評(píng)論