面試必備之深入理解自旋鎖
1、自旋鎖
-
簡單回顧一下CAS演算法
-
CAS演算法 即compare and swap(比較與交換),是一種有名的無鎖演算法。無鎖程式設計,即不使用鎖的情況下實現多執行緒之間的變數同步,也就是在沒有執行緒被阻塞的情況下實現變數的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS演算法涉及到三個運算元
-
需要讀寫的記憶體值 V
-
進行比較的值 A
-
擬寫入的新值 B
-
當且僅當 V 的值等於 A時,CAS通過原子方式用新值B來更新V的值,否則不會執行任何操作(比較和替換是一個原子操作)。一般情況下是一個自旋操作,即不斷的重試。
-
-
什麼是自旋鎖?
-
自旋鎖(spinlock):是指當一個執行緒在獲取鎖的時候,如果鎖已經被其它執行緒獲取,那麼該執行緒將迴圈等待,然後不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖才會退出迴圈。
-
獲取鎖的執行緒一直處於活躍狀態,但是並沒有執行任何有效的任務,使用這種鎖會造成busy-waiting。
-
它是為實現保護共享資源而提出一種鎖機制。其實,自旋鎖與互斥鎖比較類似,它們都是為了解決對某項資源的互斥使用。無論是互斥鎖,還是自旋鎖,在任何時刻,最多隻能有一個保持者,也就說,在任何時刻最多隻能有一個執行單元獲得鎖。但是兩者在排程機制上略有不同。對於互斥鎖,如果資源已經被佔用,資源申請者只能進入睡眠狀態。但是自旋鎖不會引起呼叫者睡眠,如果自旋鎖已經被別的執行單元保持,呼叫者就一直迴圈在那裡看是否該自旋鎖的保持者已經釋放了鎖,"自旋"一詞就是因此而得名。
-
-
Java如何實現自旋鎖?
-
下面是個簡單的例子:
image
lock()方法利用的CAS,當第一個執行緒A獲取鎖的時候,能夠成功獲取到,不會進入while迴圈,如果此時執行緒A沒有釋放鎖,另一個執行緒B又來獲取鎖,此時由於不滿足CAS,所以就會進入while迴圈,不斷判斷是否滿足CAS,直到A執行緒呼叫unlock方法釋放了該鎖。
使用了CAS原子操作,lock函式將owner設定為當前執行緒,並且預測原來的值為空。unlock函式將owner設定為null,並且預測值為當前執行緒。
當有第二個執行緒呼叫lock操作時由於owner值不為空,導致迴圈一直被執行,直至第一個執行緒呼叫unlock函式將owner設定為null,第二個執行緒才能進入臨界區。
由於自旋鎖只是將當前執行緒不停地執行迴圈體,不進行執行緒狀態的改變,所以響應速度更快。但當執行緒數不停增加時,效能下降明顯,因為每個執行緒都需要執行,佔用CPU時間。如果執行緒競爭不激烈,並且保持鎖的時間段。適合使用自旋鎖。
注:該例子為非公平鎖,獲得鎖的先後順序,不會按照進入lock的先後順序進行。
-
-
自旋鎖存在的問題
-
如果某個執行緒持有鎖的時間過長,就會導致其它等待獲取鎖的執行緒進入迴圈等待,消耗CPU。使用不當會造成CPU使用率極高。
上面Java實現的自旋鎖不是公平的,即無法滿足等待時間最長的執行緒優先獲取鎖。不公平的鎖就會存在“執行緒飢餓”問題。
-
-
自旋鎖的優點
-
自旋鎖不會使執行緒狀態發生切換,一直處於使用者態,即執行緒一直都是active的;不會使執行緒進入阻塞狀態,減少了不必要的上下文切換,執行速度快
非自旋鎖在獲取不到鎖的時候會進入阻塞狀態,從而進入核心態,當獲取到鎖的時候需要從核心態恢復,需要執行緒上下文切換。 (執行緒被阻塞後便進入核心(Linux)排程狀態,這個會導致系統在使用者態與核心態之間來回切換,嚴重影響鎖的效能)
-
-
可重入的自旋鎖和不可重入的自旋鎖
文章開始的時候的那段程式碼,仔細分析一下就可以看出,它是不支援重入的,即當一個執行緒第一次已經獲取到了該鎖,在鎖釋放之前又一次重新嘗試獲取該鎖,第二次就不能成功獲取到。由於不滿足CAS,所以第二次嘗試獲取會進入while迴圈等待,而如果是可重入鎖,第二次也是應該能夠成功獲取到的。
而且,即使第二次能夠成功獲取,那麼當第一次釋放鎖的時候,第二次獲取到的鎖也會被釋放,而這是不合理的。
為了實現可重入鎖,我們需要引入一個計數器,用來記錄獲取鎖的執行緒數。
image
2、TicketLock
TicketLock主要解決的是公平性的問題。
-
思路:每當有執行緒獲取鎖的時候,就給該執行緒分配一個遞增的id,我們稱之為排隊號,同時,鎖對應一個服務號,每當有執行緒釋放鎖,服務號就會遞增,此時如果服務號與某個執行緒排隊號一致,那麼該執行緒就獲得鎖,由於排隊號是遞增的,所以就保證了最先請求獲取鎖的執行緒可以最先獲取到鎖,就實現了公平性。
-
可以想象成銀行辦理業務排隊,排隊的每一個顧客都代表一個需要請求鎖的執行緒,而銀行服務視窗表示鎖,每當有視窗服務完成就把自己的服務號加一,此時在排隊的所有顧客中,只有自己的排隊號與服務號一致的才可以得到服務。
image
上面的實現方式是,執行緒獲取鎖之後,將它的排隊號返回,等該執行緒釋放鎖的時候,需要將該排隊號傳入。但這樣是有風險的,因為這個排隊號是可以被修改的,一旦排隊號被不小心修改了,那麼鎖將不能被正確釋放。一種更好的實現方式如下:
image
上面的實現方式是將每個執行緒的排隊號放到了ThreadLocal中。
-
TicketLock存在的問題:
- 多處理器系統上,每個程序/執行緒佔用的處理器都在讀寫同一個變數serviceNum ,每次讀寫操作都必須在多個處理器快取之間進行快取同步,這會導致繁重的系統匯流排和記憶體的流量,大大降低系統整體的效能。
-
CLH鎖是一種基於連結串列的可擴充套件、高效能、公平的自旋鎖,申請執行緒只在本地變數上自旋,它不斷輪詢前驅的狀態,如果發現前驅釋放了鎖就結束自旋,獲得鎖。
實現程式碼如下: 
3、自旋鎖與互斥鎖
- 自旋鎖與互斥鎖都是為了實現保護資源共享的機制。
- 無論是自旋鎖還是互斥鎖,在任意時刻,都最多隻能有一個保持者。
獲取互斥鎖的執行緒,如果鎖已經被佔用,則該執行緒將進入睡眠狀態;獲取自旋鎖的執行緒則不會睡眠,而是一直迴圈等待鎖釋放。
4、總結:
-
自旋鎖:執行緒獲取鎖的時候,如果鎖被其他執行緒持有,則當前執行緒將迴圈等待,直到獲取到鎖。
-
自旋鎖等待期間,執行緒的狀態不會改變,執行緒一直是使用者態並且是活動的(active)。
-
自旋鎖如果持有鎖的時間太長,則會導致其它等待獲取鎖的執行緒耗盡CPU。
-
自旋鎖本身無法保證公平性,同時也無法保證可重入性。
-
基於自旋鎖,可以實現具備公平性和可重入性質的鎖。
-
TicketLock:採用類似銀行排號叫好的方式實現自旋鎖的公平性,但是由於不停的讀取serviceNum,每次讀寫操作都必須在多個處理器快取之間進行快取同步,這會導致繁重的系統匯流排和記憶體的流量,大大降低系統整體的效能。
-
CLHLock和MCSLock通過連結串列的方式避免了減少了處理器快取同步,極大的提高了效能,區別在於CLHLock是通過輪詢其前驅節點的狀態,而MCS則是檢視當前節點的鎖狀態。
-
CLHLock在NUMA架構下使用會存在問題。在沒有cache的NUMA系統架構中,由於CLHLock是在當前節點的前一個節點上自旋,NUMA架構中處理器訪問本地記憶體的速度高於通過網路訪問其他節點的記憶體,所以CLHLock在NUMA架構上不是最優的自旋鎖。

145天以來,Java架構更新了 428個主題,已經有91位同學加入。微信掃碼關注java架構,獲取Java面試題和架構師相關題目和視訊。上述相關面試題答案,盡在Java架構中。