天天看點

【BAT面試題系列】面試官:你了解樂觀鎖和悲觀鎖嗎?

樂觀鎖和悲觀鎖問題,是出現頻率比較高的面試題。本文将由淺入深,逐漸介紹它們的基本概念、實作方式(含執行個體)、适用場景,以及可能遇到的面試官追問,希望能夠幫助你打動面試官。

一、基本概念

二、實作方式(含執行個體)

      1、CAS(Compare And Swap)

      2、版本号機制

三、優缺點和适用場景

四、面試官追問:樂觀鎖加鎖嗎?

五、面試官追問:CAS有哪些缺點?

六、總結

樂觀鎖和悲觀鎖是兩種思想,用于解決并發場景下的資料競争問題。

樂觀鎖:樂觀鎖在操作資料時非常樂觀,認為别人不會同時修改資料。是以樂觀鎖不會上鎖,隻是在執行更新的時候判斷一下在此期間别人是否修改了資料:如果别人修改了資料則放棄操作,否則執行操作。

悲觀鎖:悲觀鎖在操作資料時比較悲觀,認為别人會同時修改資料。是以操作資料時直接把資料鎖住,直到操作完成後才會釋放鎖;上鎖期間其他人不能修改資料。

在說明實作方式之前,需要明确:樂觀鎖和悲觀鎖是兩種思想,它們的使用是非常廣泛的,不局限于某種程式設計語言或資料庫。

悲觀鎖的實作方式是加鎖,加鎖既可以是對代碼塊加鎖(如Java的synchronized關鍵字),也可以是對資料加鎖(如MySQL中的排它鎖)。

樂觀鎖的實作方式主要有兩種:CAS機制和版本号機制,下面詳細介紹。

CAS操作包括了3個操作數:

需要讀寫的記憶體位置(V)

進行比較的預期值(A)

拟寫入的新值(B)

CAS操作邏輯如下:如果記憶體位置V的值等于預期的A值,則将該位置更新為新值B,否則不進行任何操作。許多CAS的操作是自旋的:如果操作不成功,會一直重試,直到操作成功為止。

這裡引出一個新的問題,既然CAS包含了Compare和Swap兩個操作,它又如何保證原子性呢?答案是:CAS是由CPU支援的原子操作,其原子性是在硬體層面進行保證的。

下面以Java中的自增操作(i++)為例,看一下悲觀鎖和CAS分别是如何保證線程安全的。我們知道,在Java中自增操作不是原子操作,它實際上包含三個獨立的操作:(1)讀取i值;(2)加1;(3)将新值寫回i

是以,如果并發執行自增操作,可能導緻計算結果的不準确。在下面的代碼示例中:value1沒有進行任何線程安全方面的保護,value2使用了樂觀鎖(CAS),value3使用了悲觀鎖(synchronized)。運作程式,使用1000個線程同時對value1、value2和value3進行自增操作,可以發現:value2和value3的值總是等于1000,而value1的值常常小于1000。

首先來介紹AtomicInteger。AtomicInteger是java.util.concurrent.atomic包提供的原子類,利用CPU提供的CAS操作來保證原子性;除了AtomicInteger外,還有AtomicBoolean、AtomicLong、AtomicReference等衆多原子類。

下面看一下AtomicInteger的源碼,了解下它的自增操作getAndIncrement()是如何實作的(源碼以Java7為例,Java8有所不同,但思想類似)。

源碼分析說明如下:

(1)getAndIncrement()實作的自增操作是自旋CAS操作:在循環中進行compareAndSet,如果執行成功則退出,否則一直執行。

(2)其中compareAndSet是CAS操作的核心,它是利用Unsafe對象實作的。

(3)Unsafe又是何許人也呢?Unsafe是用來幫助Java通路作業系統底層資源的類(如可以配置設定記憶體、釋放記憶體),通過Unsafe,Java具有了底層操作能力,可以提升運作效率;強大的底層資源操作能力也帶來了安全隐患(類的名字Unsafe也在提醒我們這一點),是以正常情況下使用者無法使用。AtomicInteger在這裡使用了Unsafe提供的CAS功能。

(4)valueOffset可以了解為value在記憶體中的偏移量,對應了CAS三個操作數(V/A/B)中的V;偏移量的獲得也是通過Unsafe實作的。

(5)value域的volatile修飾符:Java并發程式設計要保證線程安全,需要保證原子性、可視性和有序性;CAS操作可以保證原子性,而volatile可以保證可視性和一定程度的有序性;在AtomicInteger中,volatile和CAS一起保證了線程安全性。關于volatile作用原理的說明涉及到Java記憶體模型(JMM),這裡不詳細展開。

說完了AtomicInteger,再說synchronized。synchronized通過對代碼塊加鎖來保證線程安全:在同一時刻,隻能有一個線程可以執行代碼塊中的代碼。synchronized是一個重量級的操作,不僅是因為加鎖需要消耗額外的資源,還因為線程狀态的切換會涉及作業系統核心态和使用者态的轉換;不過随着JVM對鎖進行的一系列優化(如自旋鎖、輕量級鎖、鎖粗化等),synchronized的性能表現已經越來越好。

除了CAS,版本号機制也可以用來實作樂觀鎖。版本号機制的基本思路是在資料中增加一個字段version,表示該資料的版本号,每當資料被修改,版本号加1。當某個線程查詢資料時,将該資料的版本号一起查出來;當該線程更新資料時,判斷目前版本号與之前讀取的版本号是否一緻,如果一緻才進行操作。

需要注意的是,這裡使用了版本号作為判斷資料變化的标記,實際上可以根據實際情況選用其他能夠标記資料版本的字段,如時間戳等。

下面以“更新玩家金币數”為例(資料庫為MySQL,其他資料庫同理),看看悲觀鎖和版本号機制是如何應對并發問題的。

考慮這樣一種場景:遊戲系統需要更新玩家的金币數,更新後的金币數依賴于目前狀态(如金币數、等級等),是以更新前需要先查詢玩家目前狀态。

下面的實作方式,沒有進行任何線程安全方面的保護。如果有其他線程在query和update之間更新了玩家的資訊,會導緻玩家金币數的不準确。

為了避免這個問題,悲觀鎖通過加鎖解決這個問題,代碼如下所示。在查詢玩家資訊時,使用select …… for update進行查詢;該查詢語句會為該玩家資料加上排它鎖,直到事務送出或復原時才會釋放排它鎖;在此期間,如果其他線程試圖更新該玩家資訊或者執行select for update,會被阻塞。

版本号機制則是另一種思路,它為玩家資訊增加一個字段:version。在初次查詢玩家資訊時,同時查詢出version資訊;在執行update操作時,校驗version是否發生了變化,如果version變化,則不進行更新。

樂觀鎖和悲觀鎖并沒有優劣之分,它們有各自适合的場景;下面從兩個方面進行說明。

與悲觀鎖相比,樂觀鎖适用的場景受到了更多的限制,無論是CAS還是版本号機制。

例如,CAS隻能保證單個變量操作的原子性,當涉及到多個變量時,CAS是無能為力的,而synchronized則可以通過對整個代碼塊加鎖來處理。再比如版本号機制,如果query的時候是針對表1,而update的時候是針對表2,也很難通過簡單的版本号來實作樂觀鎖。

如果悲觀鎖和樂觀鎖都可以使用,那麼選擇就要考慮競争的激烈程度:

當競争不激烈 (出現并發沖突的機率小)時,樂觀鎖更有優勢,因為悲觀鎖會鎖住代碼塊或資料,其他線程無法同時通路,影響并發,而且加鎖和釋放鎖都需要消耗額外的資源。

當競争激烈(出現并發沖突的機率大)時,悲觀鎖更有優勢,因為樂觀鎖在執行更新時頻繁失敗,需要不斷重試,浪費CPU資源。

筆者在面試時,曾遇到面試官如此追問。下面是我對這個問題的了解:

(1)樂觀鎖本身是不加鎖的,隻是在更新時判斷一下資料是否被其他線程更新了;AtomicInteger便是一個例子。

(2)有時樂觀鎖可能與加鎖操作合作,例如,在前述updateCoins()的例子中,MySQL在執行update時會加排它鎖。但這隻是樂觀鎖與加鎖操作合作的例子,不能改變“樂觀鎖本身不加鎖”這一事實。

面試到這裡,面試官可能已經中意你了。不過面試官準備對你發起最後的進攻:你知道CAS這種實作方式有什麼缺點嗎?

下面是CAS一些不那麼完美的地方:

假設有兩個線程——線程1和線程2,兩個線程按照順序進行以下操作:

(1)線程1讀取記憶體中資料為A;

(2)線程2将該資料修改為B;

(3)線程2将該資料修改為A;

(4)線程1對資料進行CAS操作

在第(4)步中,由于記憶體中資料仍然為A,是以CAS操作成功,但實際上該資料已經被線程2修改過了。這就是ABA問題。

在AtomicInteger的例子中,ABA似乎沒有什麼危害。但是在某些場景下,ABA卻會帶來隐患,例如棧頂問題:一個棧的棧頂經過兩次(或多次)變化又恢複了原值,但是棧可能已發生了變化。

對于ABA問題,比較有效的方案是引入版本号,記憶體中的值每發生一次變化,版本号都+1;在進行CAS操作時,不僅比較記憶體中的值,也會比較版本号,隻有當二者都沒有變化時,CAS才能執行成功。Java中的AtomicStampedReference類便是使用版本号來解決ABA問題的。

在并發沖突機率大的高競争環境下,如果CAS一直失敗,會一直重試,CPU開銷較大。針對這個問題的一個思路是引入退出機制,如重試次數超過一定門檻值後失敗退出。當然,更重要的是避免在高競争環境下使用樂觀鎖。

CAS的功能是比較受限的,例如CAS隻能保證單個變量(或者說單個記憶體值)操作的原子性,這意味着:(1)原子性不一定能保證線程安全,例如在Java中需要與volatile配合來保證線程安全;(2)當涉及到多個變量(記憶體值)時,CAS也無能為力。

除此之外,CAS的實作需要硬體層面處理器的支援,在Java中普通使用者無法直接使用,隻能借助atomic包下的原子類使用,靈活性受到限制。

本文介紹了樂觀鎖和悲觀鎖的基本概念、實作方式(含執行個體)、适用場景,以及可能遇到的面試官追問,希望能夠對你面試有幫助。最後,祝大家都拿到心儀的offer!

【BAT面試題系列】面試官:你了解樂觀鎖和悲觀鎖嗎?

https://www.cnblogs.com/qjjazry/p/6581568.html

https://segmentfault.com/a/1190000016611415

https://www.cnblogs.com/pkufork/p/java_unsafe.html

https://stackoverflow.com/questions/19660737/aba-in-lock-free-algorithms

https://www.zhihu.com/question/23281499

創作不易,如果文章對你有幫助,就點個贊、評個論呗~

繼續閱讀