亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

樂觀鎖與悲觀鎖

1. 前言

本節內容主要是對 Java 樂觀鎖與悲觀鎖進行更加深入的講解,本節內容更加偏重于對樂觀鎖的講解,因為 synchronized 悲觀鎖對于大部分學習者并不陌生,本節主要內容如下:

  • 樂觀鎖與悲觀鎖的概念,之前有所講解,這里用很小的篇幅進行知識的回顧,鞏固;
  • 樂觀鎖與悲觀鎖的使用場景介紹,通過理解悲觀鎖與樂觀鎖不同的風格,理解什么場景下需要選擇合適的鎖,為本節的重點內容之一;
  • 了解樂觀鎖的缺點,樂觀鎖有自己的特定的缺陷,不同的鎖都有自己的優點與缺點;
  • 了解樂觀鎖缺陷的解決方式,作為本節內容的重點之一;
  • 通過引入 Atomic 操作,實現樂觀鎖,為本節內容的核心,通過對比 synchronized 的實現,用兩種鎖機制實現同一個需求。

本節內容為 CAS 原理的進階講解,也是樂觀鎖與悲觀鎖的深入講解。因為對于并發編程,悲觀鎖與樂觀鎖的涉及頻率非常高,所以對其進行更加深入的講解。

2. 樂觀鎖與悲觀鎖的概念

悲觀鎖:總是假設最壞的情況,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣其他線程想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完后再把資源轉讓給其它線程)。

樂觀鎖:總是假設最好的情況,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號機制和 CAS 算法實現。

樂觀鎖適用于多讀的應用類型,這樣可以提高吞吐量,像數據庫提供的類似于 write_condition 機制,其實都是提供的樂觀鎖。

3. 樂觀鎖與悲觀鎖的使用場景

簡單的來說 CAS 適用于寫比較少的情況下(多讀場景,沖突一般較少),synchronized 適用于寫比較多的情況下(多寫場景,沖突一般較多)。

  • 對于資源競爭較少(線程沖突較輕)的情況,使用 synchronized 同步鎖進行線程阻塞和喚醒切換以及用戶態內核態間的切換操作額外浪費消耗 CPU 資源;而 CAS 基于硬件實現,不需要進入內核,不需要切換線程,操作自旋幾率較少,因此可以獲得更高的性能;
  • 對于資源競爭嚴重(線程沖突嚴重)的情況,CAS 自旋的概率會比較大,從而浪費更多的 CPU 資源,效率低于 synchronized。

總結:樂觀鎖適用于寫比較少的情況下(多讀場景),即沖突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。

但如果是多寫的情況,一般會經常產生沖突,這就會導致上層應用會不斷地進行 retry,這樣反倒是降低了性能,所以一般多寫的場景下用悲觀鎖就比較合適。

4. 樂觀鎖的缺點

ABA 問題:我們之前也對此進行過介紹。

如果一個變量 V 初次讀取的時候是 A 值,并且在準備賦值的時候檢查到它仍然是 A 值,那我們就能說明它的值沒有被其他線程修改過了嗎?

很明顯是不能的,因為在這段時間它的值可能被改為其他值,然后又改回 A,那 CAS 操作就會誤認為它從來沒有被修改過。這個問題被稱為 CAS 操作的 “ABA” 問題。

循環時間長開銷大:在特定場景下會有效率問題。

自旋 CAS(也就是不成功就一直循環執行直到成功)如果長時間不成功,會給 CPU 帶來非常大的執行開銷。

總結:我們這里主要關注 ABA 問題。循環時間長開銷大的問題,在特定場景下很難避免的,因為所有的操作都需要在合適自己的場景下才能發揮出自己特有的優勢。

5. ABA 問題解決之版本號機制

講解 CAS 原理時,對于解決辦法進行了簡要的介紹,僅僅是一筆帶過。這里進行較詳細的闡釋。其實 ABA 問題的解決,我們通常通過如下方式進行解決:版本號機制。我們一起來看下版本號機制:

版本號機制:一般是在數據中加上一個數據版本號 version 字段,表示數據被修改的次數,當數據被修改時,version 值會加 1。當線程 A 要更新數據值時,在讀取數據的同時也會讀取 version 值,在提交更新時,若剛才讀取到的 version 值為當前數據中的 version 值相等時才更新,否則重試更新操作,直到更新成功。

場景示例:假設商店類 Shop 中有一個 version 字段,當前值為 1 ;而當前商品數量為 50。

  • 店員 A 此時將其讀出( version=1 ),并將商品數量扣除 10,更新為 50 - 10 = 40;
  • 在店員 A 操作的過程中,店員 B 也讀入此信息( version=1 ),并將商品數量扣除 20,更新為 50 - 20 = 30;
  • 店員 A 完成了修改工作,將數據版本號加 1( version=2 ),商品數量為 40,提交更新,此時由于提交數據版本大于記錄當前版本,數據被更新,數據記錄 version 更新為 2 ;
  • 店員 B 完成了操作,也將版本號加 1( version=2 ),試圖更新商品數量為 30。但此時比對數據記錄版本時發現,店員 B 提交的數據版本號為 2 ,數據記錄當前版本也為 2 ,不滿足 “ 提交版本必須大于記錄當前版本才能執行更新 “ 的樂觀鎖策略,因此,店員 B 的提交被駁回;
  • 店員 B 再次重新獲取數據,version = 2,商品數量 40。在這個基礎上繼續執行自己扣除 20 的操作,商品數量更新為 40 - 20 = 20;
  • 店員 B 將版本號加 1 ,version = 3,將之前的記錄 version 2 更新為 3 ,將之前的數量 40 更新 為 20。

從如上描述來看,所有的操作都不會出現臟數據,關鍵在于版本號的控制。

Tips:Java 對于樂觀鎖的使用進行了良好的封裝,我們可以直接使用并發編程包來進行樂觀鎖的使用。本節接下來所使用的 Atomic 操作即為封裝好的操作。

之所以還要對 CAS 原理以及 ABA 問題進行深入的分析,主要是為了讓學習者了解底層的原理,以便更好地在不同的場景下選擇使用鎖的類型。

6. Atomic 操作實現樂觀鎖

為了更好地理解悲觀鎖與樂觀鎖,我們通過設置一個簡單的示例場景來進行分析。并且我們采用悲觀鎖 synchronized 和樂觀鎖 Atomic 操作進行分別實現。

Atomic 操作類,指的是 java.util.concurrent.atomic 包下,一系列以 Atomic 開頭的包裝類。例如 AtomicBoolean,AtomicInteger,AtomicLong。它們分別用于 Boolean,Integer,Long 類型的原子性操作。

Atomic 操作的底層實現正是利用的 CAS 機制,而 CAS 機制即樂觀鎖。

場景設計

  • 創建兩個線程,創建方式可自選;
  • 定義一個全局共享的 static int 變量 count,初始值為 0;
  • 兩個線程同時操作 count,每次操作 count 加 1;
  • 每個線程做 100 次 count 的增加操作。

結果預期:最終 count 的值應該為 200。

悲觀鎖 synchronized 實現

public class DemoTest extends Thread{
    private static int count = 0; //定義count = 0
    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) { //通過for循環創建兩個線程
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    //每個線程讓count自增100次
                    for (int i = 0; i < 100; i++) {
                        synchronized (DemoTest.class){
                            count++;
                        }
                    }
                }
            }). start();
        }
        try{
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(count);
    }
}

結果驗證

200

樂觀鎖 Atomic 操作實現

public class DemoTest extends Thread{
    //Atomic 操作,引入AtomicInteger。這是實現樂觀鎖的關鍵所在。
    private static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    //每個線程讓count自增100次
                    for (int i = 0; i < 100; i++) {
                        count.incrementAndGet();
                    }
                }
            }). start();
        }
        try{
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(count);
    }
}

結果驗證

200

代碼解讀
此處主要關注兩個點,第一個是 count 的創建,是通過 AtomicInteger 進行的實例化,這是使用 Atomic 的操作的入口,也是使用 CAS 樂觀鎖的一個標志。

第二個是需要關注 count 的增加 1 調用是 AtomicInteger 中 的 incrementAndGet 方法,該方法是原子性操作,遵循 CAS 原理。

7. 小結

本節內容所有的知識點講解都可以作為重點內容進行學習。悲觀鎖與樂觀鎖是并發編程中所涉及的非常重要的內容,一定要深入的理解和掌握。

對于課程中 CAS 原理的進階講解,也是非常重要的知識點,對于 ABA 問題,是并發編程中所涉及的高頻話題、考題,也要對此加以理解和掌握。