[Java 并發(fā)]讀寫鎖ReedWriteLock/StampedLock

這篇看一下JUC包提供的讀寫鎖(共享鎖/獨(dú)占鎖)吹榴。

之前我們都知道在一個(gè)變量被讀或者寫數(shù)據(jù)的時(shí)候每次只有一個(gè)線程可以執(zhí)行洒放,那么今天我們來看一下讀寫鎖净当,讀寫兩不誤ReadWriteLock衙伶。

這里有兩個(gè)概念:

獨(dú)占鎖:

指該鎖一次只能被一個(gè)線程所持有。(ReentrantLock和Synchronized都屬于獨(dú)占鎖)叨咖。

共享鎖:

指該鎖可被多個(gè)線程所持有瘩例。

ReentrantReadWriteLock其讀鎖是共享鎖,共寫鎖是獨(dú)占鎖芒澜。

讀鎖的共享鎖可以保證并發(fā)讀是非常高效的,讀寫创淡,寫讀痴晦,寫寫的過程是互斥的。

直接使用ReentrantReadWriteLock寫段代碼看一下:

class CacheList{
    private volatile ArrayList<Long> list = new ArrayList<>();

    private ReadWriteLock lock = new ReentrantReadWriteLock();

    public void put(Long value) {
        try {
            lock.writeLock().lock(); // 獲取寫鎖
            System.out.println(Thread.currentThread().getName() + " \t 開始寫入數(shù)據(jù): \t" + value);

            TimeUnit.SECONDS.sleep(2); // 阻塞兩秒
            this.list.add(value);

            System.out.println(Thread.currentThread().getName() + " \t 寫入數(shù)據(jù)完成");
            lock.writeLock().unlock(); // 釋放寫鎖
        }catch (Exception ex) {
            ex.printStackTrace();
        }

    }

    public void get() {
        try {
            lock.readLock().lock(); // 獲取讀鎖
            System.out.println(Thread.currentThread().getName() + " \t 開始讀取數(shù)據(jù)");

            TimeUnit.SECONDS.sleep(2); // 阻塞兩秒
            String collect = this.list.stream().map(String::valueOf).collect(Collectors.joining(","));

            System.out.println(Thread.currentThread().getName() + " \t 讀取數(shù)據(jù)完成: " + collect);
            lock.readLock().unlock(); // 釋放讀鎖
        } catch (Exception ex) {
            ex.printStackTrace();
        }

    }
}


public class ReadWriteLockDemo {

    public static void main(String[] args) {
        CacheList cacheMap = new CacheList();

        IntStream.range(0, 5)
                .forEach(i -> new Thread(() ->  cacheMap.put(System.currentTimeMillis()),
                        "寫線程:" + i).start());

        IntStream.range(0, 5)
                .forEach(i -> new Thread(cacheMap::get,
                        "讀線程:" + i).start());
    }
}

上方代碼運(yùn)行效果如下:

image-20191224235044062

可以看到運(yùn)行結(jié)果琳彩,紅色圈住的地方我們可以看到當(dāng)使用寫鎖的時(shí)候不管是哪個(gè)線程進(jìn)來都會(huì)使其他線程在外等待誊酌,直到鎖被釋放才能擁有獲取權(quán)限。而藍(lán)色部分是使用了讀鎖露乏,所有線程可以同時(shí)獲取允許多個(gè)線程同時(shí)擁有鎖碧浊。

注:

但是會(huì)出現(xiàn)寫一個(gè)問題,就是寫?zhàn)囸I現(xiàn)象瘟仿,上方我們是先運(yùn)行了所有的寫線程箱锐,讀線程是在寫線程后執(zhí)行的,假如讀線程的數(shù)量大于寫線程數(shù)量的話劳较,因鎖的大概率都被讀線程執(zhí)行了驹止,就會(huì)造成一種寫?zhàn)囸I現(xiàn)象,寫線程無法滿足大量讀線程的讀操作观蜗,因?yàn)閷懢€程少的時(shí)候會(huì)搶不到鎖臊恋。

然而在JDK1.8新增了一個(gè)鎖叫做StampedLock鎖,他是對ReadWriteLock的改進(jìn)。

上邊也說了ReadWrite鎖可能會(huì)出現(xiàn)寫?zhàn)囸I墓捻,而StampedLock就是為了解決這個(gè)問題鎖設(shè)計(jì)的抖仅,StampedLock可以選擇使用樂觀鎖或悲觀鎖。

樂觀鎖:每次去拿數(shù)據(jù)的時(shí)候,并不是獲取鎖對象撤卢,而是為了判斷標(biāo)記為(stamp)是否又被修改环凿,如果有修改就再去獲取讀一次。

悲觀鎖:每次拿數(shù)據(jù)的時(shí)候都去獲取鎖凸丸。

通過樂觀鎖拷邢,當(dāng)寫線程沒有寫數(shù)據(jù)的時(shí)候,標(biāo)志位stamp并沒有改變屎慢,所以即使有再多的讀線程讀數(shù)據(jù)瞭稼,他都可以讀取,而無需獲取鎖腻惠,這就不會(huì)使得寫線程搶不到鎖了环肘。

stamp類似一個(gè)時(shí)間戳的作用,每次寫的時(shí)候?qū)ζ?1來改變被操作對象的stamp值集灌。

通過代碼來操作下看一看悔雹,先寫一個(gè)出現(xiàn)寫?zhàn)囸I的情況,模擬19個(gè)讀線程讀取數(shù)據(jù)欣喧,1個(gè)寫線程寫數(shù)據(jù)腌零。

class CacheList{
    private volatile ArrayList<Long> list = new ArrayList<>();

    private StampedLock lock = new StampedLock();

    public void put(Long value) {
        long stamped = -1; // 設(shè)置標(biāo)記位
        try {
            stamped = lock.writeLock(); // 獲取寫鎖
            System.out.println(Thread.currentThread().getName() + " \t 開始寫入數(shù)據(jù): \t" + value);

            TimeUnit.SECONDS.sleep(2); // 阻塞兩秒
            this.list.add(value);

            System.out.println(Thread.currentThread().getName() + " \t 寫入數(shù)據(jù)完成");
        }catch (Exception ex) {
            ex.printStackTrace();
        }finally {
            lock.unlockWrite(stamped); // 釋放寫鎖
        }

    }

    public void get() {
        long stamped = -1; // 設(shè)置標(biāo)記位
        try {
            stamped = lock.readLock(); // 獲取讀鎖  -->這里是悲觀鎖實(shí)現(xiàn)  --> stamped重新賦值標(biāo)記位
            System.out.println(Thread.currentThread().getName() + " \t 開始讀取數(shù)據(jù)");

            TimeUnit.SECONDS.sleep(2); // 阻塞兩秒
            String collect = this.list.stream().map(String::valueOf).collect(Collectors.joining(","));

            System.out.println(Thread.currentThread().getName() + " \t 讀取數(shù)據(jù)完成: " + collect);
        } catch (Exception ex) {
            ex.printStackTrace();
        }finally {
            lock.unlockRead(stamped); // 釋放讀鎖 --> 這里我們放入一個(gè)標(biāo)記位
        }

    }
}

public class ReadWriteLockDemo2 {

    public static void main(String[] args) {
        CacheList cacheMap = new CacheList();

        IntStream.range(0, 19)
                .forEach(i -> new Thread(cacheMap::get,
                        "讀線程:" + i).start());

        IntStream.range(0, 1)
                .forEach(i -> new Thread(() ->  cacheMap.put(System.currentTimeMillis()),
                        "寫線程:" + i).start());
    }
}

上邊使用了StampedLock做了一個(gè)讀鎖悲觀鎖的實(shí)現(xiàn),模擬了20個(gè)線程唆阿,假設(shè)了寫線程因不能及時(shí)寫入數(shù)據(jù)造成寫?zhàn)囸I現(xiàn)象益涧。我們看一下運(yùn)行結(jié)果。

image-20191225224738709

可以看到結(jié)果驯鳖,讀鎖都可以同時(shí)獲取鎖闲询,就算寫線程沒有寫入數(shù)據(jù)所有讀線程還是在搶占鎖,使用ReadWriteLock也是會(huì)出現(xiàn)同樣的現(xiàn)象浅辙,寫?zhàn)囸I扭弧。

下面我們使用 樂觀鎖,每次判斷標(biāo)記位是否被修改记舆,如果有被修改就再進(jìn)行上鎖然后重新讀取鸽捻。

class CacheList{
    private volatile ArrayList<Long> list = new ArrayList<>();

    private StampedLock lock = new StampedLock();

    public void put(Long value) {
        long stamped = -1; // 設(shè)置標(biāo)記位
        try {
            stamped = lock.writeLock(); // 獲取寫鎖
            System.out.println(Thread.currentThread().getName() + " \t 開始寫入數(shù)據(jù): \t" + value);

            TimeUnit.SECONDS.sleep(2); // 阻塞兩秒
            this.list.add(value);

            System.out.println(Thread.currentThread().getName() + " \t 寫入數(shù)據(jù)完成");
        }catch (Exception ex) {
            ex.printStackTrace();
        }finally {
            lock.unlockWrite(stamped); // 釋放寫鎖
        }

    }

    public void get() {
        // 這里使用了樂觀鎖,每次去判斷標(biāo)記位是否被改變泽腮,如果寫線程有修改此值會(huì)被修改
        long stamped = lock.tryOptimisticRead();
        try {
            System.out.println(Thread.currentThread().getName() + " \t 開始讀取數(shù)據(jù)");
            TimeUnit.SECONDS.sleep(2); // 阻塞兩秒
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        // 讀取值
        String collect = this.list.stream().map(String::valueOf).collect(Collectors.joining(","));

        // 判斷以下標(biāo)記位是否被修改泊愧,被修改就會(huì)返回false,說明有寫線程寫入了新數(shù)據(jù)
        // 那么重新獲取鎖并去讀取值盛正,否則直接使用上面讀取的值
        if (!lock.validate(stamped)){
            try {
                stamped = lock.readLock();
                collect = this.list.stream().map(String::valueOf).collect(Collectors.joining(","));
            }catch (Exception ex) {
                ex.printStackTrace();
            }finally {
                lock.unlockRead(stamped);
            }

        }

        System.out.println(Thread.currentThread().getName() + " \t 讀取數(shù)據(jù)完成: " + collect);

    }
}

public class ReadWriteLockDemo2 {

    public static void main(String[] args) {
        CacheList cacheMap = new CacheList();

        IntStream.range(0, 19)
                .forEach(i -> new Thread(cacheMap::get,
                        "讀線程:" + i).start());

        IntStream.range(0, 1)
                .forEach(i -> new Thread(() ->  cacheMap.put(System.currentTimeMillis()),
                        "寫線程:" + i).start());
    }
}

直接看運(yùn)行結(jié)果:

image-20191225231803933

主要看get方法删咱,get方法開始調(diào)用StampedLocktryOptimisticRead方法來獲取標(biāo)志位stamp,獲取樂觀鎖那塊并不是真的去上鎖(所以不會(huì)阻塞寫操作),然后直接去讀數(shù)據(jù)豪筝。接著通過validate方法來判斷標(biāo)志位是否被修改了痰滋,修改了就在進(jìn)行獲取鎖進(jìn)行讀取摘能,沒被修改則會(huì)返回true直接使用上邊獲取到的值。

StampedLock解決了在沒有新數(shù)據(jù)寫入時(shí)敲街,由于過多讀操作搶奪鎖而使得寫操作一直獲取不到鎖無法寫入新數(shù)據(jù)的問題团搞。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市多艇,隨后出現(xiàn)的幾起案子逻恐,更是在濱河造成了極大的恐慌,老刑警劉巖峻黍,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件复隆,死亡現(xiàn)場離奇詭異,居然都是意外死亡姆涩,警方通過查閱死者的電腦和手機(jī)挽拂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來骨饿,“玉大人亏栈,你說我怎么就攤上這事『曜福” “怎么了绒北?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長察署。 經(jīng)常有香客問我闷游,道長,這世上最難降的妖魔是什么箕母? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任储藐,我火速辦了婚禮俱济,結(jié)果婚禮上嘶是,老公的妹妹穿的比我還像新娘。我一直安慰自己蛛碌,他們只是感情好聂喇,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蔚携,像睡著了一般希太。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上酝蜒,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天誊辉,我揣著相機(jī)與錄音,去河邊找鬼亡脑。 笑死堕澄,一個(gè)胖子當(dāng)著我的面吹牛邀跃,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蛙紫,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼拍屑,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了坑傅?” 一聲冷哼從身側(cè)響起僵驰,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎唁毒,沒想到半個(gè)月后蒜茴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡枉证,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年矮男,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片室谚。...
    茶點(diǎn)故事閱讀 40,488評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡毡鉴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出秒赤,到底是詐尸還是另有隱情猪瞬,我是刑警寧澤,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布入篮,位于F島的核電站陈瘦,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏潮售。R本人自食惡果不足惜痊项,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望酥诽。 院中可真熱鬧鞍泉,春花似錦、人聲如沸肮帐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽训枢。三九已至托修,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間恒界,已是汗流浹背睦刃。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留十酣,地道東北人涩拙。 一個(gè)月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓枣宫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親吃环。 傳聞我的和親對象是個(gè)殘疾皇子也颤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評論 2 359