悲觀鎖、樂觀鎖傻傻分不清怎麼半?你只是還沒搞懂機制罷了

2020-10-28 12:00:47

鎖的原因都是由並行問題發生的,在此我只是寫一些面試中可能會問到的問題以及問題的答案,並不是給大家深入的講解鎖機制
一般面試官問都是從一個點引入一個點的問問題,所以我就先從執行緒問題引入到鎖問題

在這裡插入圖片描述

說說執行緒安全問題

執行緒安全是多執行緒領域的問題,執行緒安全可以簡單理解為一個方法或者一個範例可以在多執行緒環境中使用而不會出現問題

在 Java 多執行緒程式設計當中,提供了多種實現 Java 執行緒安全的方式:

最簡單的方式,使用 Synchronization 關鍵字
使用 java.util.concurrent.atomic 包中的原子類,例如 AtomicInteger
使用 java.util.concurrent.locks 包中的鎖
使用執行緒安全的集合 ConcurrentHashMap
使用 volatile 關鍵字,保證變數可見性(直接從記憶體讀,而不是從執行緒 cache 讀)

何謂悲觀鎖與樂觀鎖

在這裡插入圖片描述

樂觀鎖對應於生活中樂觀的人總是想著事情往好的方向發展,悲觀鎖對應於生活中悲觀的人總是想著事情往壞的方向發展。這兩種人各有優缺點,不能不以場景而定說一種人好於另外一種人。

悲觀鎖

悲觀鎖悲觀的認為每一次操作都會造成更新丟失問題,在每次查詢時加上排他鎖

每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會block直到它拿到鎖。傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖

  • 悲觀鎖總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞直到它拿到鎖( 共用資源每次只給一個執行緒使用,其它執行緒阻塞,用完後再把資源轉讓給其它執行緒)。傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。Java 中 synchronized 和ReentrantLock 等獨佔鎖就是悲觀鎖思想的實現。

CAS 樂觀鎖

CAS 是項樂觀鎖技術,當多個執行緒嘗試使用 CAS 同時更新同一個變數時,只有其中一個執行緒能更新變數的值,而其它執行緒都失敗,失敗的執行緒並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試

CAS 操作包含三個運算元 —— 記憶體位置(V)、預期原值(A)和新值(B)。如果記憶體位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前值。)CAS 有效地說明了「我認為位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。」這其實和樂觀鎖的衝突檢查 + 資料更新的原理是一樣的每次查詢都不會造成更新丟失,利用版本欄位控制。

  • 樂觀鎖總是假設最好的情況,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號機制和 CAS 演演算法實現。 樂觀鎖適用於多讀的應用型別,這樣可以提高吞吐量,像資料庫提供的類似於 write_condition 機制,其實都是提供的樂觀鎖。在 Java 中 java.util.concurrent.atomic 包下面的原子變數類就是使用了樂觀鎖的一種實現方式 CAS 實現的。

兩種鎖的使用場景

從上面對兩種鎖的介紹,我們知道兩種鎖各有優缺點,不可認為一種好於另一種,像 樂觀鎖適用於寫比較少的情況下(多讀場景),即衝突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果是多寫的情況,一般會經常產生衝突,這就會導致上層應用會不斷的進行 retry,這樣反倒是降低了效能,所以一般多寫的場景下用悲觀鎖就比較合適。

說說 CAS ‘ABA’ 問題

CAS 演演算法實現一個重要前提需要取出記憶體中某時刻的資料,而在下時刻比較並替換,那麼在這個時間差類會導致資料的變化

比如說一個執行緒 one 從記憶體位置 V 中取出 A,這時候另一個執行緒 two 也從記憶體中取出 A,並且 two 進行了一些操作變成了 B,然後 two 又將 V 位置的資料變成 A,這時候執行緒 one 進行 CAS 操作發現記憶體中仍然是 A,然後 one 操作成功。儘管執行緒 one 的 CAS 操作成功,但是不代表這個過程就是沒有問題的

部分樂觀鎖的實現是通過版本號(version)的方式來解決 ABA 問題,樂觀鎖每次在執行資料的修改操作時,都會帶上一個版本號,一旦版本號和資料的版本號一致就可以執行修改操作並對版本號執行 +1 操作,否則就執行失敗。因為每次操作的版本號都會隨之增加,所以不會出現 ABA 問題,因為版本號只會增加不會減少

##樂觀鎖的業務場景及實現方式

樂觀鎖(Optimistic Lock):

  • 每次獲取資料的時候,都不會擔心資料被修改,所以每次獲取資料的時候都不會進行加鎖,但是在更新資料的時候需要判斷該資料是否被別人修改過。如果資料被其他執行緒修改,則不進行資料更新,如果資料沒有被其他執行緒修改,則進行資料更新。由於資料沒有進行加鎖,期間該資料可以被其他執行緒進行讀寫操作
    比較適合讀取操作比較頻繁的場景,如果出現大量的寫入操作,資料發生衝突的可能性就會增大,為了保證資料的一致性,應用層需要不斷的重新獲取資料,這樣會增加大量的查詢操作,降低了系統的吞吐量

簡單講講自旋鎖

自旋鎖是採用讓當前執行緒不停地的在迴圈體內執行實現的,當迴圈的條件被其他執行緒改變時 才能進入臨界區

當一個執行緒 呼叫這個不可重入的自旋鎖去加鎖的時候沒問題,當再次呼叫lock()的時候,因為自旋鎖的持有參照已經不為空了,該執行緒物件會誤認為是別人的執行緒持有了自旋鎖
使用了CAS原子操作,lock函數將owner設定為當前執行緒,並且預測原來的值為空。unlock函數將owner設定為null,並且預測值為當前執行緒。
當有第二個執行緒呼叫lock操作時由於owner值不為空,導致迴圈一直被執行,直至第一個執行緒呼叫unlock函數將owner設定為null,第二個執行緒才能進入臨界區。

由於自旋鎖只是將當前執行緒不停地執行迴圈體,不進行執行緒狀態的改變,所以響應速度更快。但當執行緒數不停增加時,效能下降明顯,因為每個執行緒都需要執行,佔用CPU時間。如果執行緒競爭不激烈,並且保持鎖的時間段,那就比較適合使用自旋鎖
最新整理面試題

最後

針對最近很多人都在面試,我這邊也整理了相當多的面試專題資料,也有其他大廠的面經。希望可以幫助到大家。

最新整理面試題

有需要的小夥伴可以加群1149778920 暗號:qf

在這裡插入圖片描述

真實面試經歷

在這裡插入圖片描述

最新整理大廠面試檔案

在這裡插入圖片描述
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援。一鍵三連哦!