溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何使用高性能解決線程饑餓的利器StampedLock

發(fā)布時(shí)間:2021-10-20 17:01:35 來源:億速云 閱讀:165 作者:iii 欄目:web開發(fā)

本篇內(nèi)容介紹了“如何使用高性能解決線程饑餓的利器StampedLock”的有關(guān)知識(shí),在實(shí)際案例的操作過程中,不少人都會(huì)遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

特性

它的設(shè)計(jì)初衷是作為一個(gè)內(nèi)部工具類,用于開發(fā)其他線程安全的組件,提升系統(tǒng)性能,并且編程模型也比ReentrantReadWriteLock  復(fù)雜,所以用不好就很容易出現(xiàn)死鎖或者線程安全等莫名其妙的問題。

三種訪問數(shù)據(jù)模式:

  • Writing(獨(dú)占寫鎖):writeLock 方法會(huì)使線程阻塞等待獨(dú)占訪問,可類比ReentrantReadWriteLock  的寫鎖模式,同一時(shí)刻有且只有一個(gè)寫線程獲取鎖資源;

  • Reading(悲觀讀鎖):readLock方法,允許多個(gè)線程同時(shí)獲取悲觀讀鎖,悲觀讀鎖與獨(dú)占寫鎖互斥,與樂觀讀共享。

  • Optimistic Reading(樂觀讀):這里需要注意了,是樂觀讀,并沒有加鎖。也就是不會(huì)有 CAS 機(jī)制并且沒有阻塞線程。僅當(dāng)當(dāng)前未處于  Writing 模式 tryOptimisticRead才會(huì)返回非 0  的郵戳(Stamp),如果在獲取樂觀讀之后沒有出現(xiàn)寫模式線程獲取鎖,則在方法validate返回 true  ,允許多個(gè)線程獲取樂觀讀以及讀鎖。同時(shí)允許一個(gè)寫線程獲取寫鎖。

支持讀寫鎖相互轉(zhuǎn)換

ReentrantReadWriteLock 當(dāng)線程獲取寫鎖后可以降級(jí)成讀鎖,但是反過來則不行。

StampedLock提供了讀鎖和寫鎖相互轉(zhuǎn)換的功能,使得該類支持更多的應(yīng)用場(chǎng)景。

注意事項(xiàng)

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. StampedLock是不可重入鎖,如果當(dāng)前線程已經(jīng)獲取了寫鎖,再次重復(fù)獲取的話就會(huì)死鎖;

  3. 都不支持 Conditon 條件將線程等待;

  4. StampedLock 的寫鎖和悲觀讀鎖加鎖成功之后,都會(huì)返回一個(gè) stamp;然后解鎖的時(shí)候,需要傳入這個(gè) stamp。

詳解樂觀讀帶來的性能提升

那為何 StampedLock 性能比 ReentrantReadWriteLock 好?

關(guān)鍵在于StampedLock 提供的樂觀讀,我們知道ReentrantReadWriteLock  支持多個(gè)線程同時(shí)獲取讀鎖,但是當(dāng)多個(gè)線程同時(shí)讀的時(shí)候,所有的寫線程都是阻塞的。

StampedLock  的樂觀讀允許一個(gè)寫線程獲取寫鎖,所以不會(huì)導(dǎo)致所有寫線程阻塞,也就是當(dāng)讀多寫少的時(shí)候,寫線程有機(jī)會(huì)獲取寫鎖,減少了線程饑餓的問題,吞吐量大大提高。

這里可能你就會(huì)有疑問,竟然同時(shí)允許多個(gè)樂觀讀和一個(gè)先線程同時(shí)進(jìn)入臨界資源操作,那讀取的數(shù)據(jù)可能是錯(cuò)的怎么辦?

是的,樂觀讀不能保證讀取到的數(shù)據(jù)是最新的,所以將數(shù)據(jù)讀取到局部變量的時(shí)候需要通過 lock.validate(stamp)  校驗(yàn)是否被寫線程修改過,若是修改過則需要上悲觀讀鎖,再重新讀取數(shù)據(jù)到局部變量。

同時(shí)由于樂觀讀并不是鎖,所以沒有線程喚醒與阻塞導(dǎo)致的上下文切換,性能更好。

其實(shí)跟數(shù)據(jù)庫的“樂觀鎖”有異曲同工之妙,它的實(shí)現(xiàn)思想很簡(jiǎn)單。我們舉個(gè)數(shù)據(jù)庫的例子。

在生產(chǎn)訂單的表 product_doc 里增加了一個(gè)數(shù)值型版本號(hào)字段 version,每次更新 product_doc 這個(gè)表的時(shí)候,都將 version  字段加 1。

select id,... ,version from product_doc where id = 123

在更新的時(shí)候匹配 version 才執(zhí)行更新。

update product_doc set version = version + 1,... where id = 123 and version = 5

數(shù)據(jù)庫的樂觀鎖就是查詢的時(shí)候?qū)?version 查出來,更新的時(shí)候利用 version 字段驗(yàn)證,若是相等說明數(shù)據(jù)沒有被修改,讀取的數(shù)據(jù)是安全的。

這里的 version 就類似于 StampedLock 的 Stamp。

使用示例

模仿寫一個(gè)將用戶 id 與用戶名數(shù)據(jù)保存在 共享變量 idMap 中,并且提供 put 方法添加數(shù)據(jù)、get 方法獲取數(shù)據(jù)、以及  putIfNotExist 先從 map 中獲取數(shù)據(jù),若沒有則模擬從數(shù)據(jù)庫查詢數(shù)據(jù)并放到 map 中。

public class CacheStampedLock {     /**      * 共享變量數(shù)據(jù)      */     private final Map<Integer, String> idMap = new HashMap<>();     private final StampedLock lock = new StampedLock();      /**      * 添加數(shù)據(jù),獨(dú)占模式      */     public void put(Integer key, String value) {         long stamp = lock.writeLock();         try {             idMap.put(key, value);         } finally {             lock.unlockWrite(stamp);         }     }      /**      * 讀取數(shù)據(jù),只讀方法      */     public String get(Integer key) {         // 1. 嘗試通過樂觀讀模式讀取數(shù)據(jù),非阻塞         long stamp = lock.tryOptimisticRead();         // 2. 讀取數(shù)據(jù)到當(dāng)前線程棧         String currentValue = idMap.get(key);         // 3. 校驗(yàn)是否被其他線程修改過,true 表示未修改,否則需要加悲觀讀鎖         if (!lock.validate(stamp)) {             // 4. 上悲觀讀鎖,并重新讀取數(shù)據(jù)到當(dāng)前線程局部變量             stamp = lock.readLock();             try {                 currentValue = idMap.get(key);             } finally {                 lock.unlockRead(stamp);             }         }         // 5. 若校驗(yàn)通過,則直接返回?cái)?shù)據(jù)         return currentValue;     }      /**      * 如果數(shù)據(jù)不存在則從數(shù)據(jù)庫讀取添加到 map 中,鎖升級(jí)運(yùn)用      * @param key      * @param value 可以理解成從數(shù)據(jù)庫讀取的數(shù)據(jù),假設(shè)不會(huì)為 null      * @return      */     public String putIfNotExist(Integer key, String value) {         // 獲取讀鎖,也可以直接調(diào)用 get 方法使用樂觀讀         long stamp = lock.readLock();         String currentValue = idMap.get(key);         // 緩存為空則嘗試上寫鎖從數(shù)據(jù)庫讀取數(shù)據(jù)并寫入緩存         try {             while (Objects.isNull(currentValue)) {                 // 嘗試升級(jí)寫鎖                 long wl = lock.tryConvertToWriteLock(stamp);                 // 不為 0 升級(jí)寫鎖成功                 if (wl != 0L) {                     // 模擬從數(shù)據(jù)庫讀取數(shù)據(jù), 寫入緩存中                     stamp = wl;                     currentValue = value;                     idMap.put(key, currentValue);                     break;                 } else {                     // 升級(jí)失敗,釋放之前加的讀鎖并上寫鎖,通過循環(huán)再試                     lock.unlockRead(stamp);                     stamp = lock.writeLock();                 }             }         } finally {             // 釋放最后加的鎖             lock.unlock(stamp);         }         return currentValue;     } }

上面的使用例子中,需要引起注意的是 get()和 putIfNotExist()  方法,第一個(gè)使用了樂觀讀,使得讀寫可以并發(fā)執(zhí)行,第二個(gè)則是使用了讀鎖轉(zhuǎn)換成寫鎖的編程模型,先查詢緩存,當(dāng)不存在的時(shí)候從數(shù)據(jù)庫讀取數(shù)據(jù)并添加到緩存中。

在使用樂觀讀的時(shí)候一定要按照固定模板編寫,否則很容易出 bug,我們總結(jié)下樂觀讀編程模型的模板:

public void optimisticRead() {     // 1. 非阻塞樂觀讀模式獲取版本信息     long stamp = lock.tryOptimisticRead();     // 2. 拷貝共享數(shù)據(jù)到線程本地棧中     copyVaraibale2ThreadMemory();     // 3. 校驗(yàn)樂觀讀模式讀取的數(shù)據(jù)是否被修改過     if (!lock.validate(stamp)) {         // 3.1 校驗(yàn)未通過,上讀鎖         stamp = lock.readLock();         try {             // 3.2 拷貝共享變量數(shù)據(jù)到局部變量             copyVaraibale2ThreadMemory();         } finally {             // 釋放讀鎖             lock.unlockRead(stamp);         }     }     // 3.3 校驗(yàn)通過,使用線程本地棧的數(shù)據(jù)進(jìn)行邏輯操作     useThreadMemoryVarables(); }

使用場(chǎng)景和注意事項(xiàng)

對(duì)于讀多寫少的高并發(fā)場(chǎng)景  StampedLock的性能很好,通過樂觀讀模式很好的解決了寫線程“饑餓”的問題,我們可以使用StampedLock  來代替ReentrantReadWriteLock ,但是需要注意的是 StampedLock 的功能僅僅是 ReadWriteLock  的子集,在使用的時(shí)候,還是有幾個(gè)地方需要注意一下。

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. StampedLock是不可重入鎖,使用過程中一定要注意;

  3. 悲觀讀、寫鎖都不支持條件變量 Conditon ,當(dāng)需要這個(gè)特性的時(shí)候需要注意;

  4. 如果線程阻塞在 StampedLock 的 readLock() 或者 writeLock() 上時(shí),此時(shí)調(diào)用該阻塞線程的 interrupt()  方法,會(huì)導(dǎo)致 CPU 飆升。所以,使用 StampedLock 一定不要調(diào)用中斷操作,如果需要支持中斷功能,一定使用可中斷的悲觀讀鎖  readLockInterruptibly() 和寫鎖 writeLockInterruptibly()。這個(gè)規(guī)則一定要記清楚。

原理分析

如何使用高性能解決線程饑餓的利器StampedLock

StapedLock局部變量

我們發(fā)現(xiàn)它并不像其他鎖一樣通過定義內(nèi)部類繼承  AbstractQueuedSynchronizer抽象類然后子類實(shí)現(xiàn)模板方法實(shí)現(xiàn)同步邏輯。但是實(shí)現(xiàn)思路還是有類似,依然使用了 CLH  隊(duì)列來管理線程,通過同步狀態(tài)值 state 來標(biāo)識(shí)鎖的狀態(tài)。

其內(nèi)部定義了很多變量,這些變量的目的還是跟 ReentrantReadWriteLock 一樣,將狀態(tài)為按位切分,通過位運(yùn)算對(duì) state  變量操作用來區(qū)分同步狀態(tài)。

比如寫鎖使用的是第八位為 1 則表示寫鎖,讀鎖使用 0-7 位,所以一般情況下獲取讀鎖的線程數(shù)量為 1-126,超過以后,會(huì)使用  readerOverflow int 變量保存超出的線程數(shù)。

自旋優(yōu)化

對(duì)多核 CPU 也進(jìn)行一定優(yōu)化,NCPU 獲取核數(shù),當(dāng)核數(shù)目超過 1  的時(shí)候,線程獲取鎖的重試、入隊(duì)錢的重試都有自旋操作。主要就是通過內(nèi)部定義的一些變量來判斷,如圖所示。

等待隊(duì)列

隊(duì)列的節(jié)點(diǎn)通過 WNode 定義,如上圖所示。等待隊(duì)列的節(jié)點(diǎn)相比 AQS 更簡(jiǎn)單,只有三種狀態(tài)分別是:

  • 0:初始狀態(tài);

  • -1:等待中;

  • 取消;

另外還有一個(gè)字段 cowait ,通過該字段指向一個(gè)棧,保存讀線程。結(jié)構(gòu)如圖所示

如何使用高性能解決線程饑餓的利器StampedLock

WNode

同時(shí)定義了兩個(gè)變量分別指向頭結(jié)點(diǎn)與尾節(jié)點(diǎn)。

/** Head of CLH queue */ private transient volatile WNode whead; /** Tail (last) of CLH queue */ private transient volatile WNode wtail;

另外有一個(gè)需要注意點(diǎn)就是 cowait, 保存所有的讀節(jié)點(diǎn)數(shù)據(jù),使用的是頭插法。

當(dāng)讀寫線程競(jìng)爭(zhēng)形成等待隊(duì)列的數(shù)據(jù)如下圖所示:

如何使用高性能解決線程饑餓的利器StampedLock

隊(duì)列

獲取寫鎖

public long writeLock() {     long s, next;  // bypass acquireWrite in fully unlocked case only     return ((((s = state) & ABITS) == 0L &&              U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?             next : acquireWrite(false, 0L)); }

獲取寫鎖,如果獲取失敗則構(gòu)建節(jié)點(diǎn)放入隊(duì)列,同時(shí)阻塞線程,需要注意的時(shí)候該方法不響應(yīng)中斷,如需中斷需要調(diào)用  writeLockInterruptibly()。否則會(huì)造成高 CPU 占用的問題。

(s = state) & ABITS 標(biāo)識(shí)讀鎖和寫鎖未被使用,那么直接執(zhí)行 U.compareAndSwapLong(this, STATE,  s, next = s + WBIT)) CAS 操作將第八位設(shè)置 1,標(biāo)識(shí)寫鎖占用成功。CAS 失敗的話則調(diào)用 acquireWrite(false,  0L)加入等待隊(duì)列,同時(shí)將線程阻塞。

另外acquireWrite(false, 0L) 方法很復(fù)雜,運(yùn)用大量自旋操作,比如自旋入隊(duì)列。

獲取讀鎖

public long readLock() {     long s = state, next;  // bypass acquireRead on common uncontended case     return ((whead == wtail && (s & ABITS) < RFULL &&              U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?             next : acquireRead(false, 0L)); }

獲取讀鎖關(guān)鍵步驟

(whead == wtail && (s & ABITS) < RFULL如果隊(duì)列為空并且讀鎖線程數(shù)未超過限制,則通過  U.compareAndSwapLong(this, STATE, s, next = s + RUNIT))CAS 方式修改 state  標(biāo)識(shí)獲取讀鎖成功。

否則調(diào)用 acquireRead(false, 0L) 嘗試使用自旋獲取讀鎖,獲取不到則進(jìn)入等待隊(duì)列。

acquireRead

當(dāng) A 線程獲取了寫鎖,B 線程去獲取讀鎖的時(shí)候,調(diào)用 acquireRead 方法,則會(huì)加入阻塞隊(duì)列,并阻塞 B  線程。方法內(nèi)部依然很復(fù)雜,大致流程梳理后如下:

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. 如果寫鎖未被占用,則立即嘗試獲取讀鎖,通過 CAS 修改狀態(tài)為標(biāo)志成功則直接返回。

  3. 如果寫鎖被占用,則將當(dāng)前線程包裝成 WNode 讀節(jié)點(diǎn),并插入等待隊(duì)列。如果是寫線程節(jié)點(diǎn)則直接放入隊(duì)尾,否則放入隊(duì)尾專門存放讀線程的 WNode  cowait 指向的棧。棧結(jié)構(gòu)是頭插法的方式插入數(shù)據(jù),最終喚醒讀節(jié)點(diǎn),從棧頂開始。

釋放鎖無論是 unlockRead 釋放讀鎖還是 unlockWrite釋放寫鎖,總體流程基本都是通過 CAS 操作,修改 state 成功后調(diào)用  release 方法喚醒等待隊(duì)列的頭結(jié)點(diǎn)的后繼節(jié)點(diǎn)線程。

想將頭結(jié)點(diǎn)等待狀態(tài)設(shè)置為 0 ,標(biāo)識(shí)即將喚醒后繼節(jié)點(diǎn)。

喚醒后繼節(jié)點(diǎn)通過 CAS 方式獲取鎖,如果是讀節(jié)點(diǎn)則會(huì)喚醒 cowait 鎖指向的棧所有讀節(jié)點(diǎn)。

釋放讀鎖

unlockRead(long stamp) 如果傳入的 stamp 與鎖持有的 stamp 一致,則釋放非排它鎖,內(nèi)部主要是通過自旋 + CAS 修改  state 成功,在修改 state 之前做了判斷是否超過讀線程數(shù)限制,若是小于限制才通過 CAS 修改 state 同步狀態(tài),接著調(diào)用 release  方法喚醒 whead 的后繼節(jié)點(diǎn)。

釋放寫鎖

unlockWrite(long stamp) 如果傳入的 stamp 與鎖持有的 stamp 一致,則釋放寫鎖,whead 不為空,且當(dāng)前節(jié)點(diǎn)狀態(tài)  status != 0 則調(diào)用 release 方法喚醒頭結(jié)點(diǎn)的后繼節(jié)點(diǎn)線程。

總結(jié)

StampedLock 并不能完全代替ReentrantReadWriteLock  ,在讀多寫少的場(chǎng)景下因?yàn)闃酚^讀的模式,允許一個(gè)寫線程獲取寫鎖,解決了寫線程饑餓問題,大大提高吞吐量。

在使用樂觀讀的時(shí)候需要注意按照編程模型模板方式去編寫,否則很容易造成死鎖或者意想不到的線程安全問題。

它不是可重入鎖,且不支持條件變量 Conditon。并且線程阻塞在 readLock() 或者 writeLock() 上時(shí),此時(shí)調(diào)用該阻塞線程的  interrupt() 方法,會(huì)導(dǎo)致 CPU 飆升。如果需要中斷線程的場(chǎng)景,一定要注意調(diào)用悲觀讀鎖 readLockInterruptibly() 和寫鎖  writeLockInterruptibly()。

另外喚醒線程的規(guī)則和 AQS 類似,先喚醒頭結(jié)點(diǎn),不同的是 StampedLock 喚醒的節(jié)點(diǎn)是讀節(jié)點(diǎn)的時(shí)候,會(huì)喚醒此讀節(jié)點(diǎn)的 cowait  鎖指向的棧的所有讀節(jié)點(diǎn),但是喚醒與插入的順序相反。

“如何使用高性能解決線程饑餓的利器StampedLock”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI