樂觀鎖對應于生活中樂觀的人總是想著事情往好的方向發展,悲觀鎖對應于生活中悲觀的人總是想著事情往壞的方向發展。
一、引入概念
1、悲觀鎖
總是假設最壞的情況,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完后再把資源轉讓給其它線程)。傳統的關系型數據庫里邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。Java中synchronized
和ReentrantLock
等獨占鎖就是悲觀鎖思想的實現。
2、樂觀鎖
總是假設最好的情況,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號機制和CAS算法實現。樂觀鎖適用于多讀的應用類型,這樣可以提高吞吐量,像數據庫提供的類似于write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic
包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。
3、兩種鎖的使用場景
從上面對兩種鎖的介紹,我們知道兩種鎖各有優缺點,不可認為一種好于另一種,像樂觀鎖適用于寫比較少的情況下(多讀場景),即沖突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果是多寫的情況,一般會經常產生沖突,這就會導致上層應用會不斷的進行retry,這樣反倒是降低了性能,所以一般多寫的場景下用悲觀鎖就比較合適。
二、樂觀鎖的實現方式——CAS算法
1、CAS算法
什么是CAS?
CAS全稱為Compare And Swap即比較并交換,其算法公式如下:
函數公式:CAS(V,E,N)V:表示要更新的變量E:表示預期值N:表示新值
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-jpYccXsQ-1600049416983)(https://pics3.baidu.com/feed/810a19d8bc3eb135116a411f9e3739d6fc1f4497.jpeg?token=c57722e5bdc2b5d2e02a24affb2e2ac9&s=6AAC3C6203AEC5EF5CF530CE000080B1)]CAS
如果V值等于E值,則將V的值設為N。若V值和E值不同,則說明已經有其他線程做了更新,則當前線程什么都不做。通俗的理解就是CAS操作需要我們提供一個期望值,當期望值與當前線程的變量值相同時,說明還沒線程修改該值,當前線程可以進行修改,也就是執行CAS操作,但如果期望值與當前線程不符,則說明該值已被其他線程修改,此時不執行更新操作,但可以選擇重新讀取該變量再嘗試再次修改該變量,也可以放棄操作。
2、“ABA問題”與“版本號機制”
CAS 會導致“ABA 問題”。CAS 算法實現一個重要前提需要取出內存中某時刻的數據,而在下時刻比較并替換,那么在這個時間差類會導致數據的變化。
比如說一個線程 one 從內存位置 V 中取出 A,這時候另一個線程 two 也從內存中取出 A,并且two 進行了一些操作變成了 B,然后 two 又將 V 位置的數據變成 A,這時候線程 one 進行 CAS 操作發現內存中仍然是 A,然后 one 操作成功。盡管線程 one 的 CAS 操作成功,但是不代表這個過程就是沒有問題的。
部分樂觀鎖的實現是通過版本號(version)的方式來解決 ABA 問題,樂觀鎖每次在執行數據的修改操作時,都會帶上一個版本號,一旦版本號和數據的版本號一致就可以執行修改操作并對版本號執行+1 操作,否則就執行失敗。因為每次操作的版本號都會隨之增加,所以不會出現 ABA 問題,因為版本號只會增加不會減少。
3、Java中的CAS
JDK5增加java.util.concurrent包,其中很多類使用了CAS操作。這些CAS操作基于Unsafe類中的native方法實現:
//第一個參數o為給定對象,offset為對象內存的偏移量,通過這個偏移量迅速定位字段并設置或獲取該字段的值,
//expected表示期望值,x表示要設置的值,下面3個方法都通過CAS原子指令執行操作,
//設置成功返回true,否則返回false。
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);
由于CAS作用的對象在主存里而不是在線程的高速緩存里,CAS操作在Java中需要配合volatile使用。
Java中的CAS主要包含以下幾個問題:
- ABA問題,即變量V初次讀時是A值,被賦值時也是A值,但期間變量被賦值成B值,CAS會誤認為他從沒被修改過。AtomicStampedReference和AtomicMarckableReference類提供了監測ABA問題的能力,其中的compareAndSet方法首先檢查當前引用是否等于預期引用,并且當前標志等于預期標志,全部相等則以原子方式將該引用和該標志的值設置為給定的更新值。
- 循環開銷,自旋CAS長時間不成功會給CPU帶來非常大的執行開銷。若JVM能支持pause命令,效率有一定提升。因為pause命令一方面可以延遲流水線執行命令,使CPU不會消耗過多的執行資源,另一方面可以避免退出循環時由內存順序沖突引起的CPU流水線被沖突,從而提高CPU的執行效率。
- 只能保證一個共享變量的原子操作,當操作涉及跨多個共享變量時CAS無效。可用AtomicReference封裝多個字段來保證引用對象之間的原子性。
三、悲觀鎖——synchronized
1、synchronized
synchronized是Java中的關鍵字,是一種同步鎖。可修飾實例方法,靜態方法,代碼塊。
- 修飾實例方法:對當前實例加鎖,進入同步代碼前要獲得當前實例的鎖
- 修飾靜態方法:對當前類對象加鎖,進入同步代碼前要獲得當前類對象的鎖
- 修飾代碼塊:指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。
2、CAS與synchronized的使用情景
- 簡單的來說CAS適用于寫比較少的情況下(多讀場景,沖突一般較少),
- synchronized適用于寫比較多的情況下(多寫場景,沖突一般較多)
- 對于資源競爭較少(線程沖突較輕)的情況,使用synchronized同步鎖進行線程阻塞和喚醒切換以及用戶態內核態間的切換操作額外浪費消耗cpu資源;而CAS基于硬件實現,不需要進入內核,不需要切換線程,操作自旋幾率較少,因此可以獲得更高的性能。
- 對于資源競爭嚴重(線程沖突嚴重)的情況,CAS自旋的概率會比較大,從而浪費更多的CPU資源,效率低于synchronized。
補充: Java并發編程這個領域中synchronized關鍵字一直都是元老級的角色,很久之前很多人都會稱它為 “重量級鎖” 。但是,在JavaSE 1.6之后進行了主要包括為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的 偏向鎖 和 輕量級鎖 以及其它各種優化之后變得在某些情況下并不是那么重了。synchronized的底層實現主要依靠 Lock-Free 的隊列,基本思路是 自旋后阻塞,競爭切換后繼續競爭鎖,稍微犧牲了公平性,但獲得了高吞吐量。在線程沖突較少的情況下,可以獲得和CAS類似的性能;而線程沖突嚴重的情況下,性能遠高于CAS。