Java併發編程之概念六:樂觀鎖與悲觀鎖

爲什麼需要鎖(併發控制)在多用戶環境中,在同一時間可能會有多個用戶更新相同的記錄,這會產生衝突。這就是著名的併發性問題。

典型的衝突有: 
(1)丟失更新:一個事務的更新覆蓋了其它事務的更新結果,就是所謂的更新丟失。例如:用戶A把值從6改爲2,用戶B把值從2改爲6,則用戶A丟失了他的更新。

(2)髒讀:當一個事務讀取其它完成一半事務的記錄時,就會發生髒讀取。例如:用戶A,B看到的值都是6,用戶B把值改爲2,用戶A讀到的值仍爲6。

悲觀鎖:所謂悲觀鎖,顧名思義就是採用一種悲觀的態度來對待事務併發問題,總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。傳統的關係型數據庫裏邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。

Select * from Account where ...(where condition).. for update.

 比如Java裏面的同步原語synchronized關鍵字的實現也是悲觀鎖

原因:Java在JDK1.5之前都是靠 synchronized關鍵字保證同步的,這種通過使用一致的鎖定協議來協調對共享狀態的訪問,可以確保無論哪個線程持有共享變量的鎖,都採用獨佔的方式來訪問這些變量。這就是一種獨佔鎖,獨佔鎖其實就是一種悲觀鎖,所以可以說 synchronized 是悲觀鎖。

悲觀鎖機制存在以下問題:  

1. 在多線程競爭下,加鎖、釋放鎖會導致比較多的上下文切換和調度延時,引起性能問題。

2. 一個線程持有鎖會導致其它所有需要此鎖的線程掛起。

3. 如果一個優先級高的線程等待一個優先級低的線程釋放鎖會導致優先級倒置,引起性能風險。

樂觀鎖:顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號,時間戳,基於所有屬性檢測等機制。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像數據庫提供的類似於write_condition機制,其實都是提供的樂觀鎖。它的基本思想就是每次提交一個事務更新時,我們想看看要修改的東西從上次讀取以後有沒有被其它事務修改過,如果修改過,那麼更新就會失敗。因爲樂觀鎖其實並不會鎖定任何記錄,所以數據庫的事務隔離級別設置爲讀取已提交或者更低的隔離界別,那麼是不能避免不可重複讀問題的(因爲此時讀事務不會阻塞其它事務),所以採用樂觀鎖的時候,系統應該要容許不可重複讀問題的出現。

 樂觀鎖機制簡介:

版本(Version)字段:在我們的實體中增加一個版本控制字段,每次事務更新後就將版本字段的值加1.

Update Account set version = version+1.....(another field) 
where version =?...(another contidition)

時間戳(timestamps):採取這種策略後,當每次要提交更新的時候就會將系統當前時間和實體加載時的時間進行比較,如果不一致,那麼就報告樂觀鎖失敗,從而回滾事務或者重新嘗試提交。採用時間戳有一些不足,比如在集羣環境下,每個節點的時間同步也許會成問題,並且如果併發事務間隔時間小於當前平臺最小的時鐘單位,那麼就會發生覆蓋前一個事務結果的問題。因此一般採用版本字段比較好。
基於所有屬性進行檢測:採用這種策略的時候,需要比較每個字段在讀取以後有沒有被修改過,所以這種策略實現起來比較麻煩,要求對每個屬性都進行比較,如果採用hibernate的話,因爲Hibernate在一級緩存中可以進行髒檢測,那麼可以判斷哪些字段被修改過,從而動態的生成sql語句進行更新。

CAS:

樂觀鎖的具體實現細節主要就是兩個步驟:衝突檢測和數據更新。其實現方式有一種比較典型的就是 Compare and Swap ( CAS )。

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

  CAS 操作中包含三個操作數 —— 需要讀寫的內存位置(V)、進行比較的預期原值(A)和擬寫入的新值(B)。如果內存位置V的值與預期原值A相匹配,那麼處理器會自動將該位置值更新爲新值B。否則處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前值。)CAS 有效地說明了“ 我認爲位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。 ”這其實和樂觀鎖的衝突檢查+數據更新的原理是一樣的。

  這裏再強調一下,樂觀鎖是一種思想。CAS是這種思想的一種實現方式。

在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。

eg:java.util.concurrent.atomic.AtomicInteger

測試代碼路徑:https://download.csdn.net/download/baidu_25310663/10982348

鎖的使用場景:

在實際生產環境裏邊,如果併發量不大且不允許髒讀,可以使用悲觀鎖解決併發問題;但如果系統的併發非常大的話,悲觀鎖定會帶來非常大的性能問題,所以我們就要選擇樂觀鎖定的方法. 

經典案例分析:

如一個金融系統,當某個操作員讀取用戶的數據,並在讀出的用戶數據的基礎上進行修改時(如更改用戶帳戶餘額),如果採用悲觀鎖機制,也就意味着整個操作過程中(從操作員讀出數據、開始修改直至提交修改結果的全過程,甚至還包括操作員中途去煮咖啡的時間),數據庫記錄始終處於加鎖狀態,可以想見,如果面對幾百上千個併發,這樣的情況將導致怎樣的後果。

樂觀鎖機制在一定程度上解決了這個問題。

樂觀鎖,大多是基於數據版本( Version )記錄機制實現。何謂數據版本?即爲數據增加一個版本標識,在基於數據庫表的版本解決方案中,一般是通過爲數據庫表增加一個 “version” 字段來實現。

讀取出數據時,將此版本號一同讀出,之後更新時,對此版本號加一。此時,將提交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,如果提交的數據版本號大於數據庫表當前版本號,則予以更新,否則認爲是過期數據。

對於上面修改用戶帳戶信息的例子而言,假設數據庫中帳戶信息表中有一個version 字段,當前值爲 1 ; 
而當前帳戶餘額字段( balance )爲 $100 。

1 操作員 A 此時將其讀出( version=1 ),並從其帳戶餘額中扣除 50(50(100-$50 )。

2 在操作員 A 操作的過程中,操作員 B 也讀入此用戶信息( version=1 ),並從其帳戶餘額中扣除 20(20(100-$20 )。

3 操作員 A 完成了修改工作,將數據版本號加一( version=2 ),連同帳戶扣除後餘額( balance=$50 ),提交至數據庫更新,此時由於提交數據版本大於數據庫記錄當前版本,數據被更新,數據庫記錄 version 更新爲 2 。

4 操作員 B 完成了操作,也將版本號加一( version=2 )試圖向數據庫提交數據( balance=$80 ),但此時比對數據庫記錄版本時發現,操作員 B 提交的數據版本號爲 2 ,數據庫記錄當前版本也爲 2 ,不滿足 “ 提交版本必須大於記錄當前版本才能執行更新 “ 的樂觀鎖策略,因此,操作員 B 的提交被駁回。這樣,就避免了操作員 B 用基於 version=1 的舊數據修改的結果覆蓋操作員 A 的操作結果的可能。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章