面經手冊 · 第16篇《碼農會鎖,ReentrantLock之公平鎖講解和實現》

小傅哥發表於2020-11-05


作者:小傅哥
部落格:https://bugstack.cn
專題:面經手冊

沉澱、分享、成長,讓自己和他人都能有所收穫!?

一、前言

Java學多少才能找到工作?

最近經常有小夥伴問我,以為我的經驗來看,學多少夠,好像更多的是看你的野心有多大。如果你只是想找個10k以內的二線城市的工作,那還是比較容易的。也不需要學資料結構、也不需要會演算法、也需要懂原始碼、更不要有多少專案經驗。

但反之我遇到一個國內大學TOP2畢業的娃,這貨兼職是Offer收割機:騰訊、阿里、位元組還有國外新加坡的工作機會等等,薪資待遇也是賊高,可能超過你對白菜價的認知。上學無用、學習無用,純屬扯淡!

你能在這條路上能付出的越多,能努力的越早,收穫就會越大!

二、面試題

謝飛機,小記,剛去冬巴拉泡完腳放鬆的飛機,因為耐克襪子丟了,罵罵咧咧的赴約面試官。

面試官:咋了,飛機,怎麼看上去不高興。

謝飛機:沒事,沒事,我心思我學的 synchronized 呢!

面試官:那正好,飛機你會鎖嗎?

謝飛機:啊。。。我沒去會所呀!!!你咋知道

面試官:我說 Java 鎖,你想啥呢!你瞭解公平鎖嗎,知道怎麼實現的嗎,給我說說!

謝飛機:公平鎖!?嗯,是不 ReentrantLock 中用到了,我怎麼感覺似乎有印象,但是不記得了。

面試官:哎,回家搜搜 CLH 吧!

三、ReentrantLock 和 公平鎖

1. ReentrantLock 介紹

鑑於上一篇小傅哥已經基於,HotSpot虛擬機器原始碼分析 synchronized 實現和相應核心知識點,本來想在本章直接介紹下 ReentrantLock。但因為 ReentrantLock 知識點較多,因此會分幾篇分別講解,突出每一篇重點,避免豬八戒吞棗。

介紹:ReentrantLock 是一個可重入且獨佔式鎖,具有與 synchronized 監視器(monitor enter、monitor exit)鎖基本相同的行為和語意。但與 synchronized 相比,它更加靈活、強大、增加了輪訓、超時、中斷等高階功能以及可以建立公平和非公平鎖。

2. ReentrantLock 知識鏈條

圖 16-1 ReentrantLock 鎖知識鏈條

ReentrantLock 是基於 Lock 實現的可重入鎖,所有的 Lock 都是基於 AQS 實現的,AQS 和 Condition 各自維護不同的物件,在使用 Lock 和 Condition 時,其實就是兩個佇列的互相移動。它所提供的共享鎖、互斥鎖都是基於對 state 的操作。而它的可重入是因為實現了同步器 Sync,在 Sync 的兩個實現類中,包括了公平鎖和非公平鎖。

這個公平鎖的具體實現,就是我們本章節要介紹的重點,瞭解什麼是公平鎖、公平鎖的具體實現。學習完基礎的知識可以更好的理解 ReentrantLock

3. ReentrantLock 公平鎖程式碼

3.1 初始化

ReentrantLock lock = new ReentrantLock(true);  // true:公平鎖
lock.lock();
try {
    // todo
} finally {
    lock.unlock();
}
  • 初始化建構函式入參,選擇是否為初始化公平鎖。
  • 其實一般情況下並不需要公平鎖,除非你的場景中需要保證順序性。
  • 使用 ReentrantLock 切記需要在 finally 中關閉,lock.unlock()

3.2 公平鎖、非公平鎖,選擇

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
  • 建構函式中選擇公平鎖(FairSync)、非公平鎖(NonfairSync)。

3.3 hasQueuedPredecessors

static final class FairSync extends Sync {

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
		...
    }
}
  • 公平鎖和非公平鎖,主要是在方法 tryAcquire 中,是否有 !hasQueuedPredecessors() 判斷。

3.4 佇列首位判斷

public final boolean hasQueuedPredecessors() {
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
  • 在這個判斷中主要就是看當前執行緒是不是同步佇列的首位,是:true、否:false
  • 這部分就涉及到了公平鎖的實現,CLH(Craig,Landin andHagersten)。三個作者的首字母組合

四、什麼是公平鎖

圖 16-2 公共廁所排隊入坑

公平鎖就像是馬路邊上的衛生間,上廁所需要排隊。當然如果有人不排隊,那麼就是非公平鎖了,比如領導要先上。

CLH 是一種基於單向連結串列的高效能、公平的自旋鎖。AQS中的佇列是CLH變體的虛擬雙向佇列(FIFO),AQS是通過將每條請求共享資源的執行緒封裝成一個節點來實現鎖的分配。

為了更好的學習理解 CLH 的原理,就需要有實踐的程式碼。接下來一 CLH 為核心分別介紹4種公平鎖的實現,從而掌握最基本的技術棧知識。

五、公平鎖實現

1. CLH

1.1 看圖說話

圖 16-3 CLH 實現過程原理圖

1.2 程式碼實現

public class CLHLock implements Lock {

    private final ThreadLocal<CLHLock.Node> prev;
    private final ThreadLocal<CLHLock.Node> node;
    private final AtomicReference<CLHLock.Node> tail = new AtomicReference<>(new CLHLock.Node());

    private static class Node {
        private volatile boolean locked;
    }

    public CLHLock() {
        this.prev = ThreadLocal.withInitial(() -> null);
        this.node = ThreadLocal.withInitial(CLHLock.Node::new);
    }

    @Override
    public void lock() {
        final Node node = this.node.get();
        node.locked = true;
        Node pred_node = this.tail.getAndSet(node);
        this.prev.set(pred_node);
        // 自旋
        while (pred_node.locked);
    }

    @Override
    public void unlock() {
        final Node node = this.node.get();
        node.locked = false;
        this.node.set(this.prev.get());
    }

}

1.3 程式碼講解

CLH(Craig,Landin and Hagersten),是一種基於連結串列的可擴充套件、高效能、公平的自旋鎖。

在這段程式碼的實現過程中,相當於是虛擬出來一個連結串列結構,由 AtomicReference 的方法 getAndSet 進行銜接。getAndSet 獲取當前元素,設定新的元素

lock()

  • 通過 this.node.get() 獲取當前節點,並設定 locked 為 true。
  • 接著呼叫 this.tail.getAndSet(node),獲取當前尾部節點 pred_node,同時把新加入的節點設定成尾部節點。
  • 之後就是把 this.prev 設定為之前的尾部節點,也就相當於鏈路的指向。
  • 最後就是自旋 while (pred_node.locked),直至程式釋放。

unlock()

  • 釋放鎖的過程就是拆鏈,把釋放鎖的節點設定為false node.locked = false
  • 之後最重要的是把當前節點設定為上一個節點,這樣就相當於把自己的節點拆下來了,等著垃圾回收。

CLH佇列鎖的優點是空間複雜度低,在SMP(Symmetric Multi-Processor)對稱多處理器結構(一臺計算機由多個CPU組成,並共享記憶體和其他資源,所有的CPU都可以平等地訪問記憶體、I/O和外部中斷)效果還是不錯的。但在 NUMA(Non-Uniform Memory Access) 下效果就不太好了,這部分知識可以自行擴充套件。

2. MCSLock

2.1 程式碼實現

public class MCSLock implements Lock {

    private AtomicReference<MCSLock.Node> tail = new AtomicReference<>(null);
    ;
    private ThreadLocal<MCSLock.Node> node;

    private static class Node {
        private volatile boolean locked = false;
        private volatile Node next = null;
    }

    public MCSLock() {
        node = ThreadLocal.withInitial(Node::new);
    }

    @Override
    public void lock() {
        Node node = this.node.get();
        Node preNode = tail.getAndSet(node);
        if (null == preNode) {
            node.locked = true;
            return;
        }
        node.locked = false;
        preNode.next = node;
        while (!node.locked) ;
    }

    @Override
    public void unlock() {
        Node node = this.node.get();
        if (null != node.next) {
            node.next.locked = true;
            node.next = null;
            return;
        }
        if (tail.compareAndSet(node, null)) {
            return;
        }
        while (node.next == null) ;
    }

}

2.1 程式碼講解

MCS 來自於發明人名字的首字母: John Mellor-Crummey和Michael Scott。

它也是一種基於連結串列的可擴充套件、高效能、公平的自旋鎖,但與 CLH 不同。它是真的有下一個節點 next,新增這個真實節點後,它就可以只在本地變數上自旋,而 CLH 是前驅節點的屬性上自旋。

因為自旋節點的不同,導致 CLH 更適合於 SMP 架構、MCS 可以適合 NUMA 非一致儲存訪問架構。你可以想象成 CLH 更需要執行緒資料在同一塊記憶體上效果才更好,MCS 因為是在本店變數自選,所以無論資料是否分散在不同的CPU模組都沒有影響。

程式碼實現上與 CLH 沒有太多差異,這裡就不在敘述了,可以看程式碼學習。

3. TicketLock

3.1 看圖說話

圖 16-4 銀行排隊叫號圖

3.2 程式碼實現

public class TicketLock implements Lock {

    private AtomicInteger serviceCount = new AtomicInteger(0);
    private AtomicInteger ticketCount = new AtomicInteger(0);
    private final ThreadLocal<Integer> owner = new ThreadLocal<>();

    @Override
    public void lock() {
        owner.set(ticketCount.getAndIncrement());
        while (serviceCount.get() != owner.get());
    }

    @Override
    public void unlock() {
        serviceCount.compareAndSet(owner.get(), owner.get() + 1);
        owner.remove();
    }
}

3.3 程式碼講解

TicketLock 就像你去銀行、呷哺給你的一個排號卡一樣,叫到你號你才能進去。屬於嚴格的公平性實現,但是多處理器系統上,每個程式/執行緒佔用的處理器都在讀寫同一個變數,每次讀寫操作都需要進行多處理間的快取同步,非常消耗系統效能。

程式碼實現上也比較簡單,lock() 中設定擁有者的號牌,並進入自旋比對。unlock() 中使用 CAS 進行解鎖操作,並處理移除。

六、總結

  • ReentrantLock 是基於 Lock 實現的可重入鎖,對於公平鎖 CLH 的實現,只是這部分知識的冰山一角,但有這一,就可以很好熱身便於後續的學習。
  • ReentrantLock 使用起來更加靈活,可操作性也更大,但一定要在 finally 中釋放鎖,目的是保證在獲取鎖之後,最終能夠被釋放。同時不要將獲取鎖的過程寫在 try 裡面。
  • 公平鎖的實現依據不同場景和SMP、NUMA的使用,會有不同的優劣效果。在實際的使用中一般預設會選擇非公平鎖,即使是自旋也是耗費效能的,一般會用在較少等待的執行緒中,避免自旋時過長。

七、系列推薦

相關文章