高併發(鎖)

糯米๓發表於2024-04-17

鎖是用於控制多個執行緒對共享資源的訪問的機制,防止出現程式對共享資源的競態關係

執行緒安全

在擁有共享資料的多條執行緒並行執行的程式中,執行緒安全的程式碼會透過同步機制保證各個執行緒都可以正常且正確的執行,不會出現資料汙染等意外情況

執行緒的競態條件

競態條件(race condition)

競態條件(race condition)指的是兩個或者以上程序或者執行緒併發執行時,其最終的結果依賴於程序或者執行緒執行的精確時序。競爭條件會產生超出預期的情況,一般情況下我們都希望程式執行的結果是符合預期的,因此競爭條件是一種需要被避免的情形。

競爭條件分為兩類:

  • Mutex(互斥):兩個或多個程序彼此之間沒有內在的制約關係,但是由於要搶佔使用某個臨界資源(不能被多個程序同時使用的資源,如印表機,變數)而產生制約關係。
  • Synchronization(同步):兩個或多個程序彼此之間存在內在的制約關係(前一個程序執行完,其他的程序才能執行),如嚴格輪轉法。

要阻止出現競態條件的關鍵就是不能讓多個程序/執行緒同時訪問那塊共享變數。訪問共享變數的那段程式碼就是臨界區(critical section)。所有的解決方法都是圍繞這個臨界區來設計的。

執行緒安全的三大特性

1、原子性(Atomicity):原子操作是不可分割的操作,要麼全部執行成功,要麼全部不執行。在多執行緒環境下,如果多個執行緒同時執行原子操作,不會出現資料不一致的情況。例如,使用synchronized關鍵字或者Lock介面來保證關鍵程式碼塊的原子性。

2、可見性(Visibility):可見性是指當一個執行緒修改了共享變數的值,其他執行緒能夠立即看到修改後的值。在多執行緒環境下,需要保證共享變數的修改對其他執行緒是可見的,通常可以使用volatile關鍵字來實現可見性。

3、有序性(Ordering):有序性是指程式的執行順序按照程式碼的先後順序來執行,不會因為編譯器的最佳化或者CPU的亂序執行而導致結果的不確定。在多執行緒環境下,需要保證共享變數的讀寫操作是有序的,通常可以透過synchronized關鍵字或volatile關鍵字來實現有序性。

Java的記憶體模型

瞭解更多:https://www.51cto.com/article/658158.html

我們都知道JVM中每個執行緒都有自己的棧空間,共享變數會存放在主記憶體中。在併發修改變數的過程中執行緒可能會發生掛起,導致寫入到主記憶體的值發生覆蓋。導致破壞了原子性

as-if-serial語義和happen-before原則

1、as-if-serial語義的意思指:不管怎麼重排序(編譯器和處理器為了提高並行度),(單執行緒)程式的執行結果不能被改變。編譯器,runtime 和處理器都必須遵守as-if-serial語義。

為了遵守as-if-serial語義,編譯器和處理器不會對存在資料依賴關係的操作做重排序,因為這種重排序會改變執行結果。但是,如果操作之間不存在資料依賴關係,這些操作可能被編譯器和處理器重排序

2、JVM定義的Happens-Before原則是一組偏序關係:對於兩個操作A和B(共享資料),這兩個操作可以在不同的執行緒中執行。如果A Happens-Before B,那麼可以保證,當A操作執行完後,A操作的執行結果對B操作是可見的

3、as-if-serial 和 happens-before 的區別

  1. as-if-serial是針對單執行緒程式的執行結果的一致性,允許虛擬機器對單執行緒程式進行指令重排序,只要不改變執行結果。
  2. happens-before是針對多執行緒程式的執行順序的一致性,描述了多執行緒之間操作的可見性和順序關係。
  3. as-if-serial語義和happens-before這麼做的目的,都是為了在不改變程式執行結果的前提下,儘可能地提高程式執行的並行度

volatile關鍵字

volitale是Java虛擬機器提供的一種輕量級的同步機制

1、保證可見性

可見性主要存於JMM的記憶體模型當中,指當一個執行緒改變其內部的工作記憶體當中的變數後,其他記憶體是否可以感知到,因為不同的工作執行緒無法訪問到對方的工作記憶體,執行緒間的通訊必須依靠主記憶體進行同步

2、不保證原子性

當在多執行緒改變變數時,需要將變數同步到工作執行緒中;當執行緒A對變數修改時,還沒同步到主記憶體中執行緒掛起,執行緒B也對變數進行修改,這時執行緒A進行執行,就會覆蓋執行緒B的值,由於可見性,這是執行緒B也會變成執行緒A修改的值,導致一致性問題

3、禁止指令重排

在本執行緒內觀察,所有操作都是有序的(即指令重排不會導致單執行緒程式執行結果與排序前有任何差別)。在一個執行緒觀察另一個執行緒,所有操作都是無序的,無序是因為發生了指令重排序。在 Java 記憶體模型中,允許編譯器和處理器對指令進行重排序,重排序過程不會影響到單執行緒程式的執行,卻會影響到多執行緒併發執行的正確性。

想要執行緒安全必須保證原子性,可見性,有序性。而volatile只能保證可見性和有序性

記憶體屏障

  • Memory barrier 能夠讓CPU或編譯器在記憶體訪問上有序。一個 Memory barrier 之前的記憶體訪問操作必定先於其之後的完成。
  • Memory barrier是一種CPU指令,用於控制特定條件下的重排序和記憶體可見性問題。Java編譯器也會根據記憶體屏障的規則禁止重排序。
  • 有的處理器的重排序規則較嚴,無需記憶體屏障也能很好的工作,Java編譯器會在這種情況下不放置記憶體屏障。

Memory Barrier可以被分為以下幾種型別:

1、LoadLoad屏障:對於這樣的語句Load1; LoadLoad; Load2,在Load2及後續讀取操作要讀取的資料被訪問前,保證Load1要讀取的資料被讀取完畢。

2、StoreStore屏障:對於這樣的語句Store1; StoreStore; Store2,在Store2及後續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。

3、LoadStore屏障:對於這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操作被刷出前,保證Load1要讀取的資料被讀取完畢。

4、StoreLoad屏障:對於這樣的語句Store1; StoreLoad; Load2,在Load2及後續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的。在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種記憶體屏障的功能

volatile語義中的記憶體屏障

在每個volatile寫操作前插入StoreStore屏障,在寫操作後插入StoreLoad屏障;

在每個volatile讀操作前插入LoadLoad屏障,在讀操作後插入LoadStore屏障;

volatile的記憶體屏障策略非常嚴格保守,保證了執行緒可見性。

final語義中的記憶體屏障

新建物件過程中,構造體中對final域的初始化寫入(StoreStore屏障)和這個物件賦值給其他引用變數,這兩個操作不能重排序;

初次讀包含final域的物件引用和讀取這個final域(LoadLoad屏障),這兩個操作不能重排序;

Intel 64/IA-32架構下寫操作之間不會發生重排序StoreStore會被省略,這種架構下也不會對邏輯上有先後依賴關係的操作進行重排序,所以LoadLoad也會變省略。

鎖的型別

從執行緒是否需要對資源加鎖可以分為 悲觀鎖(系統)樂觀鎖(CAS)
從資源已被鎖定,執行緒是否阻塞可以分為自旋鎖(CAS)
從多個執行緒併發訪問資源,也就是 Synchronized 可以分為 無鎖偏向鎖輕量級鎖重量級鎖
從鎖的公平性進行區分,可以分為公平鎖非公平鎖
從根據鎖是否重複獲取可以分為 可重入鎖不可重入鎖
從那個多個執行緒能否獲取同一把鎖分為 共享鎖(讀鎖)排他鎖(寫鎖)

類鎖和物件鎖

類鎖是載入類上的,而類資訊是存在 JVM 方法區的,並且整個 JVM 只有一份,方法區又是所有執行緒共享的,所以類鎖是所有執行緒共享的

類鎖是指對靜態方法或靜態變數加鎖時所產生的鎖。類鎖是類上的,而類資訊是存在 JVM 方法區的,並且整個 JVM 只有一份,方法區又是所有執行緒共享的;當一個執行緒獲取了一個類鎖,其他執行緒就無法同時獲取該類的鎖,直到該執行緒釋放了鎖。

物件鎖是指對非靜態方法或非靜態變數加鎖時所產生的鎖。每個物件都有自己的物件鎖,當一個執行緒獲取了某個物件的鎖,其他執行緒就無法同時獲取該物件的鎖,直到該執行緒釋放了鎖

自旋鎖

優點:自旋鎖不會引起呼叫者休眠,如果自旋鎖已經被別的執行緒保持,呼叫者就一直迴圈在那裡看是否該自旋鎖的保持者釋放了鎖。由於自旋鎖不會引起呼叫者休眠,所以自旋鎖的效率遠高於互斥鎖

缺點:
1、自旋鎖一直佔用 CPU ,在未獲得鎖的情況下,一直執行,如果不能在很短的時間內獲得鎖,會導致 CPU 效率降低。
2、試圖遞迴地獲得自旋鎖會引起死鎖。遞迴程式決不能在持有自旋鎖時呼叫它自己,也決不能在遞迴呼叫時試圖獲得相同的自旋鎖。

CAS自旋

CAS(Compare and Swap)是一種基於原子操作的併發控制機制,用於實現多執行緒之間的同步。CAS操作包含三個運算元,分別為記憶體位置V、期望值A和新值B。當且僅當V的值等於A時,CAS將V的值設為B,否則不做任何操作。

CAS 是一條 CPU 的原子指令(cmpxchg指令),不會造成所謂的資料不一致問題,Unsafe類提供的 CAS 方法(如compareAndSwapXXX)底層實現即為CPU指令cmpxchg

  1. AtomicInteger:用於原子性地操作整數型變數。
  2. AtomicLong:用於原子性地操作長整型變數。
  3. AtomicBoolean:用於原子性地操作布林型變數。
  4. AtomicReference:用於原子性地操作引用型別變數。
  5. AtomicIntegerArray:用於原子性地操作整數型陣列。
  6. AtomicLongArray:用於原子性地操作長整型陣列。
  7. AtomicReferenceArray:用於原子性地操作引用型別陣列。

ABA問題

在多執行緒程式設計中,常常會遇到ABA問題。簡單來說,ABA問題就是指執行緒A讀取了共享變數V的值,然後執行緒B將V的值改為了其他值,再次改回了原來的值,然後執行緒A又進行了寫操作。這種情況下,執行緒A會認為V的值沒有發生變化,但實際上V的值已經發生了變化。

為了解決ABA問題,Java中提供了一個帶有時間戳的原子類AtomicStampedReference。AtomicStampedReference類可以透過增加時間戳來解決ABA問題,時間戳的作用是記錄每一次變數的修改操作,使得在比較並交換時不僅需要比較變數的值,還需要比較變數的時間戳是否相同。這樣就可以避免ABA問題的發生。

具體來說,AtomicStampedReference類中的compareAndSet方法不僅會比較當前值和期望值是否相等,還會比較當前的時間戳是否相等。只有當前值和時間戳都相等時,才會執行CAS操作,否則不會執行。

Unsafe類

Unsafe是位於sun.misc包下的一個類,主要提供一些用於執行低階別、不安全操作的方法,如直接訪問系統記憶體資源、自主管理記憶體資源等,這些方法在提升Java執行效率、增強Java語言底層資源操作能力方面起到了很大的作用

如何呼叫Unsafe類

1、從getUnsafe方法的使用限制條件出發,透過Java命令列命令-Xbootclasspath/a把呼叫Unsafe相關方法的類A所在jar包路徑追加到預設的bootstrap路徑中,使得A被引導類載入器載入,從而透過Unsafe.getUnsafe方法安全的獲取Unsafe例項。

java -Xbootclasspath/a: ${path}   // 其中path為呼叫Unsafe相關方法的類所在jar包路徑 

2、透過反射獲取單例物件theUnsafe。

private static Unsafe reflectGetUnsafe() {
    try {
      Field field = Unsafe.class.getDeclaredField("theUnsafe");
      field.setAccessible(true);
      return (Unsafe) field.get(null);
    } catch (Exception e) {
      log.error(e.getMessage(), e);
      return null;
    }
}

相關文章