天天看點

給大夥講講并發程式設計裡的悲觀鎖和樂觀鎖思維導圖悲觀鎖樂觀鎖使用場景總結

思維導圖

給大夥講講并發程式設計裡的悲觀鎖和樂觀鎖思維導圖悲觀鎖樂觀鎖使用場景總結
文章已收錄Github精選,歡迎Star: https://github.com/yehongzhi/learningSummary

悲觀鎖

悲觀鎖是平時開發中經常用到的一種鎖,比如

ReentrantLock

synchronized

等就是這種思想的展現,它總是假設别的線程在拿線程的時候都會修改資料,是以每次拿到資料的時候都會上鎖,這樣别的線程想拿這個資料就會被阻塞。如圖所示:

給大夥講講并發程式設計裡的悲觀鎖和樂觀鎖思維導圖悲觀鎖樂觀鎖使用場景總結

synchronized

是悲觀鎖的一種實作,一般我們都會有這樣使用:

private static Object monitor = new Object();

public static void main(String[] args) throws Exception {
    //鎖一段代碼塊
    synchronized (monitor){

    }
}
//鎖執行個體方法,鎖對象是this,即該類執行個體本身
public synchronized void doSome(){

}
//鎖靜态方法,鎖對象是該類,即XXX.class
public synchronized static void add(){

}           

我們以最簡單的同步代碼塊來分析,其實就是将synchronized作用于一個給定的執行個體對象monitor,即目前執行個體對象就是鎖對象,每次當線程進入synchronized包裹的代碼塊時就會要求目前線程持有monitor執行個體對象鎖,如果目前有其他線程正持有該對象鎖,那麼新到的線程就必須等待,這樣也就保證了每次隻有一個線程執行synchronized内包裹的代碼塊。

從上面的分析中可以看出,悲觀鎖是獨占和排他的,隻要操作資源都會對資源進行加鎖。假設讀多寫少的情況下,使用悲觀鎖的效果就不是很好。這時就引出了接下來要講的樂觀鎖。

樂觀鎖

樂觀鎖,顧名思義它總是假設最好的情況,線程每次去拿資料時都認為别人不會修改,是以不會上鎖,但是在更新的時候會判斷一下在此期間别人有沒有去更新這個資料,如果這個資料沒有被更新,目前線程将自己修改的資料成功寫入。如果資料已經被其他線程更新,則根據不同的實作方式執行不同的操作(例如報錯或者自動重試)。如圖所示:

給大夥講講并發程式設計裡的悲觀鎖和樂觀鎖思維導圖悲觀鎖樂觀鎖使用場景總結

一般樂觀鎖在java中是通過無鎖程式設計實作的,最常見的就是CAS算法,比如Java并發包中的原子類的遞增操作就是通過CAS算法實作的。

CAS算法,其實就是Compare And Swap(比較與交換)的意思。目的就是将記憶體的值更新為需要的值,但是有個條件,記憶體值必須與期待的原記憶體值相同。展開來說,我們有三個變量,記憶體值M,期望的記憶體值E,更新值U,隻有當M==E時,才會将M更新為U。

CAS算法實作的樂觀鎖在很多地方有應用,比如并發包的原子類AtomicInteger類。在自增的時候就使用到CAS算法。

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

//var1 是this指針
//var2 是偏移量
//var4 是自增量
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        //擷取記憶體,稱之為期待的記憶體值E
        var5 = this.getIntVolatile(var1, var2);
        //var5 + var4的結果是更新值U
        //這裡使用JNI方法,每個線程将自己記憶體中的記憶體值M與var5期望值比較,
        //如果相同則更新為var5 + var4,傳回true跳出循環。
        //如果不相同,則把記憶體值M更新為最新的記憶體值,然後自旋,直到更新成功為止
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    //傳回更新後的值
    return var5;
}

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);           

是以可以看出CAS算法其實是無鎖的。好處是在讀多寫少的情況下,性能是比較好的。那麼CAS算法的缺點其實也是很明顯的。

  • ABA問題。線程C将記憶體值A改成了B後,又改成了A,而線程D會認為記憶體值A沒有改變過,這個問題就稱為ABA問題。解決辦法很簡單,在變量前面加上版本号,每次變量更新的時候變量的版本号都

    +1

    ,即

    A->B->A

    就變成了

    1A->2B->3A

  • 在寫多讀少的情況下,也就是頻繁更新資料,那麼會導緻其他線程經常更新失敗,那麼就會進入自旋,自旋時會占用CPU資源。如果資源競争激烈,多線程自旋的時間長,導緻消耗資源。

使用場景

在讀多寫少的場景下,更新時很少發生沖突,使用樂觀鎖,減少了上鎖和釋放鎖的開銷,可以有效地提升系統的性能。

相反,在寫多讀少的場景下,如果使用樂觀鎖會導緻更新時經常産生沖突,然後線程會循環重試,這樣會增大CPU的消耗。在這種情況下,建議可以使用悲觀鎖。

總結

在日常的開發中,悲觀鎖和樂觀鎖應該是見得最多,用得最多的鎖,比如最常見的

synchronized

ReentrantLock

是悲觀鎖,并發包中的原子類和ConcurrentHashMap則用了樂觀鎖。鎖的實作并不複雜,關鍵是搞懂這兩種鎖的思想,這樣才能在合适的地方使用合适的鎖。

這篇文章就講到這裡了,希望看完後能有所收獲,感謝你的閱讀。

覺得有用就點個贊吧,你的點贊是我創作的最大動力~

我是一個努力讓大家記住的程式員。我們下期再見!!!

給大夥講講并發程式設計裡的悲觀鎖和樂觀鎖思維導圖悲觀鎖樂觀鎖使用場景總結
能力有限,如果有什麼錯誤或者不當之處,請大家批評指正,一起學習交流!