樂觀鎖與悲觀鎖
1. 前言
本節(jié)內(nèi)容主要是對 Java 樂觀鎖與悲觀鎖進行更加深入的講解,本節(jié)內(nèi)容更加偏重于對樂觀鎖的講解,因為 synchronized 悲觀鎖對于大部分學(xué)習(xí)者并不陌生,本節(jié)主要內(nèi)容如下:
- 樂觀鎖與悲觀鎖的概念,之前有所講解,這里用很小的篇幅進行知識的回顧,鞏固;
- 樂觀鎖與悲觀鎖的使用場景介紹,通過理解悲觀鎖與樂觀鎖不同的風(fēng)格,理解什么場景下需要選擇合適的鎖,為本節(jié)的重點內(nèi)容之一;
- 了解樂觀鎖的缺點,樂觀鎖有自己的特定的缺陷,不同的鎖都有自己的優(yōu)點與缺點;
- 了解樂觀鎖缺陷的解決方式,作為本節(jié)內(nèi)容的重點之一;
- 通過引入 Atomic 操作,實現(xiàn)樂觀鎖,為本節(jié)內(nèi)容的核心,通過對比 synchronized 的實現(xiàn),用兩種鎖機制實現(xiàn)同一個需求。
本節(jié)內(nèi)容為 CAS 原理的進階講解,也是樂觀鎖與悲觀鎖的深入講解。因為對于并發(fā)編程,悲觀鎖與樂觀鎖的涉及頻率非常高,所以對其進行更加深入的講解。
2. 樂觀鎖與悲觀鎖的概念
悲觀鎖:總是假設(shè)最壞的情況,每次去拿數(shù)據(jù)的時候都認為別人會修改,所以每次在拿數(shù)據(jù)的時候都會上鎖,這樣其他線程想拿這個數(shù)據(jù)就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完后再把資源轉(zhuǎn)讓給其它線程)。
樂觀鎖:總是假設(shè)最好的情況,每次去拿數(shù)據(jù)的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數(shù)據(jù),可以使用版本號機制和 CAS 算法實現(xiàn)。
樂觀鎖適用于多讀的應(yīng)用類型,這樣可以提高吞吐量,像數(shù)據(jù)庫提供的類似于 write_condition 機制,其實都是提供的樂觀鎖。
3. 樂觀鎖與悲觀鎖的使用場景
簡單的來說 CAS 適用于寫比較少的情況下(多讀場景,沖突一般較少),synchronized 適用于寫比較多的情況下(多寫場景,沖突一般較多)。
- 對于資源競爭較少(線程沖突較輕)的情況,使用 synchronized 同步鎖進行線程阻塞和喚醒切換以及用戶態(tài)內(nèi)核態(tài)間的切換操作額外浪費消耗 CPU 資源;而 CAS 基于硬件實現(xiàn),不需要進入內(nèi)核,不需要切換線程,操作自旋幾率較少,因此可以獲得更高的性能;
- 對于資源競爭嚴重(線程沖突嚴重)的情況,CAS 自旋的概率會比較大,從而浪費更多的 CPU 資源,效率低于 synchronized。
總結(jié):樂觀鎖適用于寫比較少的情況下(多讀場景),即沖突真的很少發(fā)生的時候,這樣可以省去了鎖的開銷,加大了系統(tǒng)的整個吞吐量。
但如果是多寫的情況,一般會經(jīng)常產(chǎn)生沖突,這就會導(dǎo)致上層應(yīng)用會不斷地進行 retry,這樣反倒是降低了性能,所以一般多寫的場景下用悲觀鎖就比較合適。
4. 樂觀鎖的缺點
ABA 問題:我們之前也對此進行過介紹。
如果一個變量 V 初次讀取的時候是 A 值,并且在準備賦值的時候檢查到它仍然是 A 值,那我們就能說明它的值沒有被其他線程修改過了嗎?
很明顯是不能的,因為在這段時間它的值可能被改為其他值,然后又改回 A,那 CAS 操作就會誤認為它從來沒有被修改過。這個問題被稱為 CAS 操作的 “ABA” 問題。
循環(huán)時間長開銷大:在特定場景下會有效率問題。
自旋 CAS(也就是不成功就一直循環(huán)執(zhí)行直到成功)如果長時間不成功,會給 CPU 帶來非常大的執(zhí)行開銷。
總結(jié):我們這里主要關(guān)注 ABA 問題。循環(huán)時間長開銷大的問題,在特定場景下很難避免的,因為所有的操作都需要在合適自己的場景下才能發(fā)揮出自己特有的優(yōu)勢。
5. ABA 問題解決之版本號機制
講解 CAS 原理時,對于解決辦法進行了簡要的介紹,僅僅是一筆帶過。這里進行較詳細的闡釋。其實 ABA 問題的解決,我們通常通過如下方式進行解決:版本號機制。我們一起來看下版本號機制:
版本號機制:一般是在數(shù)據(jù)中加上一個數(shù)據(jù)版本號 version 字段,表示數(shù)據(jù)被修改的次數(shù),當(dāng)數(shù)據(jù)被修改時,version 值會加 1。當(dāng)線程 A 要更新數(shù)據(jù)值時,在讀取數(shù)據(jù)的同時也會讀取 version 值,在提交更新時,若剛才讀取到的 version 值為當(dāng)前數(shù)據(jù)中的 version 值相等時才更新,否則重試更新操作,直到更新成功。
場景示例:假設(shè)商店類 Shop 中有一個 version 字段,當(dāng)前值為 1 ;而當(dāng)前商品數(shù)量為 50。
- 店員 A 此時將其讀出( version=1 ),并將商品數(shù)量扣除 10,更新為 50 - 10 = 40;
- 在店員 A 操作的過程中,店員 B 也讀入此信息( version=1 ),并將商品數(shù)量扣除 20,更新為 50 - 20 = 30;
- 店員 A 完成了修改工作,將數(shù)據(jù)版本號加 1( version=2 ),商品數(shù)量為 40,提交更新,此時由于提交數(shù)據(jù)版本大于記錄當(dāng)前版本,數(shù)據(jù)被更新,數(shù)據(jù)記錄 version 更新為 2 ;
- 店員 B 完成了操作,也將版本號加 1( version=2 ),試圖更新商品數(shù)量為 30。但此時比對數(shù)據(jù)記錄版本時發(fā)現(xiàn),店員 B 提交的數(shù)據(jù)版本號為 2 ,數(shù)據(jù)記錄當(dāng)前版本也為 2 ,不滿足 “ 提交版本必須大于記錄當(dāng)前版本才能執(zhí)行更新 “ 的樂觀鎖策略,因此,店員 B 的提交被駁回;
- 店員 B 再次重新獲取數(shù)據(jù),version = 2,商品數(shù)量 40。在這個基礎(chǔ)上繼續(xù)執(zhí)行自己扣除 20 的操作,商品數(shù)量更新為 40 - 20 = 20;
- 店員 B 將版本號加 1 ,version = 3,將之前的記錄 version 2 更新為 3 ,將之前的數(shù)量 40 更新 為 20。
從如上描述來看,所有的操作都不會出現(xiàn)臟數(shù)據(jù),關(guān)鍵在于版本號的控制。
Tips:Java 對于樂觀鎖的使用進行了良好的封裝,我們可以直接使用并發(fā)編程包來進行樂觀鎖的使用。本節(jié)接下來所使用的 Atomic 操作即為封裝好的操作。
之所以還要對 CAS 原理以及 ABA 問題進行深入的分析,主要是為了讓學(xué)習(xí)者了解底層的原理,以便更好地在不同的場景下選擇使用鎖的類型。
6. Atomic 操作實現(xiàn)樂觀鎖
為了更好地理解悲觀鎖與樂觀鎖,我們通過設(shè)置一個簡單的示例場景來進行分析。并且我們采用悲觀鎖 synchronized 和樂觀鎖 Atomic 操作進行分別實現(xiàn)。
Atomic 操作類,指的是 java.util.concurrent.atomic 包下,一系列以 Atomic 開頭的包裝類。例如 AtomicBoolean,AtomicInteger,AtomicLong。它們分別用于 Boolean,Integer,Long 類型的原子性操作。
Atomic 操作的底層實現(xiàn)正是利用的 CAS 機制,而 CAS 機制即樂觀鎖。
場景設(shè)計:
- 創(chuàng)建兩個線程,創(chuàng)建方式可自選;
- 定義一個全局共享的 static int 變量 count,初始值為 0;
- 兩個線程同時操作 count,每次操作 count 加 1;
- 每個線程做 100 次 count 的增加操作。
結(jié)果預(yù)期:最終 count 的值應(yīng)該為 200。
悲觀鎖 synchronized 實現(xiàn):
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循環(huán)創(chuàng)建兩個線程
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);
}
}
結(jié)果驗證:
200
樂觀鎖 Atomic 操作實現(xiàn):
public class DemoTest extends Thread{
//Atomic 操作,引入AtomicInteger。這是實現(xiàn)樂觀鎖的關(guān)鍵所在。
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);
}
}
結(jié)果驗證:
200
代碼解讀:
此處主要關(guān)注兩個點,第一個是 count 的創(chuàng)建,是通過 AtomicInteger 進行的實例化,這是使用 Atomic 的操作的入口,也是使用 CAS 樂觀鎖的一個標志。
第二個是需要關(guān)注 count 的增加 1 調(diào)用是 AtomicInteger 中 的 incrementAndGet 方法,該方法是原子性操作,遵循 CAS 原理。
7. 小結(jié)
本節(jié)內(nèi)容所有的知識點講解都可以作為重點內(nèi)容進行學(xué)習(xí)。悲觀鎖與樂觀鎖是并發(fā)編程中所涉及的非常重要的內(nèi)容,一定要深入的理解和掌握。
對于課程中 CAS 原理的進階講解,也是非常重要的知識點,對于 ABA 問題,是并發(fā)編程中所涉及的高頻話題、考題,也要對此加以理解和掌握。