1. 程式人生 > >MCS鎖的原理和實現

MCS鎖的原理和實現

前情回顧

上一篇文章中主要討論了自旋鎖的特點和其適用場景,然後給出了兩種自旋鎖的簡單實現。

存在的問題

無論是簡單的非公平自旋鎖還是公平的基於排隊的自旋鎖,由於執行執行緒均在同一個共享變數上自旋,申請和釋放鎖的時候必須對該共享變數進行修改,這將導致所有參與排隊自旋鎖操作的處理器的快取變得無效。如果排隊自旋鎖競爭比較激烈的話,頻繁的快取同步操作會導致繁重的系統匯流排和記憶體的流量,從而大大降低了系統整體的效能。

所以,需要有一種辦法能夠讓執行執行緒不再在同一個共享變數上自旋,避免過高頻率的快取同步操作。於是MCS和CLH鎖應運而生。

這兩個鎖的名稱都來源於發明人的名字首字母:

MCS:John Mellor-Crummey and Michael Scott。

CLH:Craig,Landin and Hagersten。

本文先介紹MCS鎖的原理和相應實現。

MCS鎖

MCS自旋鎖是一種基於單向連結串列的高效能、公平的自旋鎖,申請加鎖的執行緒只需要在本地變數上自旋,直接前驅負責通知其結束自旋,從而極大地減少了不必要的處理器快取同步的次數,降低了匯流排和記憶體的開銷。

先上實現程式碼,然後在分析重點:

public class MCSLockV2 {

    /**
     * MCS鎖節點
     */
    public static class MCSNodeV2 {

        /**
         * 後繼節點
         */
volatile MCSNodeV2 next; /** * 預設狀態為等待鎖 */ volatile boolean blocked = true; } /** * 執行緒到節點的對映 */ private ThreadLocal<MCSNodeV2> currentThreadNode = new ThreadLocal<>(); /** * 指向最後一個申請鎖的MCSNode */
volatile MCSNodeV2 queue; /** * 原子更新器 */ private static final AtomicReferenceFieldUpdater UPDATER = AtomicReferenceFieldUpdater .newUpdater( MCSLockV2.class, MCSLockV2.MCSNodeV2.class, "queue"); /** * MCS獲取鎖操作 */ public void lock() { MCSNodeV2 cNode = currentThreadNode.get(); if (cNode == null) { // 初始化節點物件 cNode = new MCSNodeV2(); currentThreadNode.set(cNode); } // 將當前申請鎖的執行緒置為queue並返回舊值 MCSNodeV2 predecessor = (MCSNodeV2) UPDATER.getAndSet(this, cNode); // step 1 if (predecessor != null) { // 形成連結串列結構(單向) predecessor.next = cNode; // step 2 // 當前執行緒處於等待狀態時自旋(MCSNode的blocked初始化為true) // 等待前驅節點主動通知,即將blocked設定為false,表示當前執行緒可以獲取到鎖 while (cNode.blocked) { } } else { // 只有一個執行緒在使用鎖,沒有前驅來通知它,所以得自己標記自己為非阻塞 - 表示已經加鎖成功 cNode.blocked = false; } } /** * MCS釋放鎖操作 */ public void unlock() { // 獲取當前執行緒對應的節點 MCSNodeV2 cNode = currentThreadNode.get(); if (cNode == null || cNode.blocked) { // 當前執行緒對應存在節點 // 並且 // 鎖擁有者進行釋放鎖才有意義 - 當blocked未true時,表示此執行緒處於等待狀態中,並沒有獲取到鎖,因此沒有權利釋放鎖 return; } if (cNode.next == null && !UPDATER.compareAndSet(this, cNode, null)) { // 沒有後繼節點的情況,將queue置為空 // 如果CAS操作失敗了表示突然有節點排在自己後面了,可能還不知道是誰,下面是等待後續者 // 這裡之所以要忙等是因為上述的lock操作中step 1執行完後,step 2可能還沒執行完 while (cNode.next == null) { } } if (cNode.next != null) { // 通知後繼節點可以獲取鎖 cNode.next.blocked = false; // 將當前節點從連結串列中斷開,方便對當前節點進行GC cNode.next = null; // for GC } // 清空當前執行緒對應的節點資訊 currentThreadNode.remove(); } /** * 測試用例 * * @param args */ public static void main(String[] args) { final MCSLockV2 lock = new MCSLockV2(); for (int i = 1; i <= 10; i++) { new Thread(generateTask(lock, String.valueOf(i))).start(); } } private static Runnable generateTask(final MCSLockV2 lock, final String taskId) { return () -> { lock.lock(); try { Thread.sleep(3000); } catch (Exception e) { } System.out.println(String.format("Thread %s Completed", taskId)); lock.unlock(); }; } }

節點定義以及鎖擁有的欄位

首先,需要定義一個節點物件。這個節點即代表了申請加鎖的執行緒之間的先後關係,節點通過其next屬性組成一個單項鍊表的資料結構。另外,節點的blocked屬性表示的是該節點是否處於等待加鎖的狀態,預設值為true,表示節點的初始狀態是等待中。

然後,鎖本身的實現有三個屬性:

  1. 節點型別queue:表示當前連結串列的尾部,每個新加入排隊的執行緒都會被放到這個位置
  2. ThreadLocal型別的currentThreadNode:儲存的是從執行緒物件到節點物件例項的對映關係
  3. 針對queue欄位的原子更新器AtomicReferenceFieldUpdater:通過AtomicReferenceFieldUpdater對queue欄位操作的一層包裝,任何操作都不會直接施加在queue上,而是通過它,它提供了一些CAS操作來保證原子性

最重要的就是其中的lock和unlock方法了。簡單分析一下實現方法:

lock方法

簡單提煉一下此方法的操作步驟和要點:

  1. 獲取當前執行緒和對應的節點物件(不存在則初始化)
  2. 將queue通過getAndSet這一原子操作更新為第一步中得到的節點物件,返回可能存在的前驅節點,如果前驅存在跳轉到Step 3;不存在跳轉到Step 4
  3. 建立單向連結串列關係,由前驅節點指向當前節點;當前執行緒開始在當前節點物件的blocked欄位上自旋等待(等待前驅節點改變其blocked的狀態)
  4. 沒有前驅節點表示此時並沒有除當前執行緒外的執行緒擁有鎖,因此可以直接改變節點的blocked為false,lock方法執行完畢表示加鎖成功

unlock方法

簡單提煉一下此方法的操作步驟和要點:

  1. 獲取當前執行緒和對應的節點物件;如果節點不存在或者節點狀態為等待的話直接返回,因為只有擁有鎖的執行緒才有資格進行釋放鎖的操作
  2. 清空當前執行緒對應的節點資訊
  3. 判斷當前節點是否擁有後繼節點,如果沒有的話跳轉到Step 4;沒有的話跳轉到Step 5
  4. 利用原子更新器的CAS操作嘗試將queue設定為null。設定成功的話表示鎖釋放成功,unlock方法執行完畢返回;設定失敗的話表示Step 3和Step 4的CAS的操作之間有別的執行緒來搗亂了,queue此時並非指向當前節點,因此需要忙等待確保連結串列結構就緒(參考程式碼註釋:lock操作的getAndSet操作和連結串列建立並非是原子性的)
  5. 此時當前節點的後繼節點已經就緒了,所以可以改變後繼節點的blocked狀態,另在其上等待的執行緒退出自旋。最後還會更新當前節點的next指向為null輔助垃圾回收

以上加鎖和釋放鎖的每個步驟都有比較詳細的註釋,相信仔細讀的話看懂並不是難事。

main方法

針對MCS鎖實現的一個用例,模擬了10個執行緒搶鎖的場景。

總結

實現的程式碼量雖然不多,但是lock和unlock的設計思想還是有些微妙之處,想要實現正確也並不容易。

需要把握的幾個重點:

  1. MCS鎖的節點物件需要有兩個狀態,next用來維護單向連結串列的結構,blocked用來表示節點的狀態,true表示處於自旋中;false表示加鎖成功
  2. MCS鎖的節點狀態blocked的改變是由其前驅節點觸發改變的
  3. 加鎖時會更新連結串列的末節點並完成連結串列結構的維護
  4. 釋放鎖的時候由於連結串列結構建立的時滯(getAndSet原子方法和連結串列建立整體而言並非原子性),可能存在多執行緒的干擾,需要使用忙等待保證連結串列結構就緒

另外需要注意的是,MCS鎖是一種不可重入的獨佔鎖。

在下一篇文章中將介紹一種更輕巧的解決方案:CLH鎖。

參考資料