“鎖”是併發編程中再熟悉不過的了。當多個線程同時操作一個變量的時候,我們需要為這個變量加上一把鎖,來保證同一時刻,只有一個線程在修改這個變量。那麼,鎖是如何工作的呢?
本文中,筆者主要談談樂觀鎖以及悲觀鎖。
先說說悲觀鎖。
筆者認為,悲觀鎖可以簡單地概括為:簡單粗暴。所謂悲觀鎖,也就是說,這個變量十分地擔心自己同一時刻被多個變量修改,為了防止這種情況的發生,會使用一把鎖,將這個變量牢牢地鎖住。也就是說,當其中一個線程,獲取到了這把鎖,那麼這個時候,其它的線程,就無法再獲取到這把鎖直到獲取鎖的線程,完成了對變量的修改之後釋放掉這把鎖。這種鎖可以說是相當安全的,但是,也是相當低效的。當一個線程正在對共享變量進行操作的時候,其它的線程,就是掛起狀態,也就是說只能乾等著,啥也幹不了。對於併發量極大的系統來說,這是完全不能接受的。
那麼有什麼辦法,來解決這個問題呢?
此時就需要使用樂觀鎖。CAS,全稱(Compare and Swap,比較並替換)。字面意思很明確了,先比較,再替換。那麼,比較啥?又替換啥?
我們知道,每一個線程,都會有自己的一個工作空間,每次工作的時候,線程會把要修改的值先從主內存中複製一份出來,然後進行修改操作之後,再寫回到主內存當中。那麼此時,對於一個線程來講,它的工作空間中的某個變量的值,跟主內存中對應的變量的值,如果是保持一致的,我們是否就可以認為,對於這個線程來說,這個共享變量沒有被其它的線程修改過呢?CAS就是這麼實現的。
對於一次CAS操作,有三個變量會被維護,它們是:期望值,老值以及新值。期望值,就是當前主內存中的共享變量的值;老值,就是當前線程工作空間中維護的值;新值,就是這個線程將要對主內存中的共享變量寫入的值。
當該線程要對共享變量進行修改的時候,我們會先檢查,當前線程的工作空間中的值和主內存中的值,是否一致,也就是上文中提到過的,如果保持一致,那麼該線程就認為,沒有其它的線程對這個變量進行過修改,它可以放心的對變量進行操作。那麼,此時第一步就是,比較老值和期望值,是否相等。如果相等,那麼此時該線程就可以安全地對共享內存進行修改。也就是,將新值,寫入到主內存中。如果不相等,那麼該線程就是獲取鎖失敗,此時,該線程就會把自己工作空間中的老值,更新為當前的期望值。Figure1引用了維基百科上的一段代碼,是CAS操作的一段C語言實現。
這段代碼中,*reg是指向主內存中對應共享變量的一個指針,reg解指針之後的值,就是共享變量的值,也就是我們所說的期望值。接著比較,當前線程工作空間中的值和期望值是否相等。如果相等,那麼該線程就可以對共享變量進行修改,也就是圖中的*reg=newval。如果不是,則該線程獲取樂觀鎖失敗,此時會返回old_reg_val,也就是期望值。該線程要做的工作就是,將自己工作空間中的值,修改為期望值。
準確地說,樂觀鎖不是一種鎖,而是一種無鎖算法,CAS就是一種無鎖算法。在實際應用中,除了CAS算法之外,還有一種做法是版本控制。常用的MySql使用的樂觀鎖便是使用了版本控制。每次對一個變量進行了一次修改之後,就會更新該共享變量的版本號。
CAS是一種CPU指令,調用CAS時,就是直接調用了該CPU指令,可以說是相當高效。然而,同樣,CAS也存在的缺陷。我們知道,CAS校驗的是主內存的值和線程自己工作空間的值。此時我們可以設想一種bad case,線程A的old value是1,線程B將內存值從1改成了2,線程C將內存值從2改成了1,這個時候,線程A試圖去進行CAS,發現,期望值和老值保持一致,允許進行變量修改。這種case顯然是不對的,這也就是樂觀鎖常說的ABA問題。當CAS獲取鎖失敗時,線程就會進入
自旋狀態,也就是我們常說的while true,在一個循環當中,不斷嘗試CAS操作來試圖獲取修改變量的資格。如果一直獲取失敗,那麼將會給CPU帶來不小的開銷!因此,對於樂觀鎖以及悲觀鎖,各有各的試用場景。寫多讀少的場景,使用悲觀鎖更加合適;相反,讀多寫少的場景,當使用樂觀鎖。