死磕 java同步系列之本身動手寫一個鎖Lock

問題

(1)本身動手寫一個鎖須要哪些知識?java

(2)本身動手寫一個鎖到底有多簡單?node

(3)本身能不能寫出來一個完美的鎖?數組

簡介

本篇文章的目標一是本身動手寫一個鎖,這個鎖的功能很簡單,能進行正常的加鎖、解鎖操做。多線程

本篇文章的目標二是經過本身動手寫一個鎖,能更好地理解後面章節將要學習的AQS及各類同步器實現的原理。學習

分析

本身動手寫一個鎖須要準備些什麼呢?測試

首先,在上一章學習synchronized的時候咱們說過它的實現原理是更改對象頭中的MarkWord,標記爲已加鎖或未加鎖。this

可是,咱們本身是沒法修改對象頭信息的,那麼咱們可不能夠用一個變量來代替呢?線程

好比,這個變量的值爲1的時候就說明已加鎖,變量值爲0的時候就說明未加鎖,我以爲可行。指針

其次,咱們要保證多個線程對上面咱們定義的變量的爭用是可控的,所謂可控即同時只能有一個線程把它的值修改成1,且當它的值爲1的時候其它線程不能再修改它的值,這種是否是就是典型的CAS操做,因此咱們須要使用Unsafe這個類來作CAS操做。code

而後,咱們知道在多線程的環境下,多個線程對同一個鎖的爭用確定只有一個能成功,那麼,其它的線程就要排隊,因此咱們還須要一個隊列。

最後,這些線程排隊的時候幹嗎呢?它們不能再繼續執行本身的程序,那就只能阻塞了,阻塞完了當輪到這個線程的時候還要喚醒,因此咱們還須要Unsfae這個類來阻塞(park)和喚醒(unpark)線程。

基於以上四點,咱們須要的神器大體有:一個變量、一個隊列、執行CAS/park/unpark的Unsafe類。

大概的流程圖以下圖所示:

mylock

關於Unsafe類的相關講解請參考彤哥以前發的文章:

死磕 java魔法類之Unsafe解析

解決

一個變量

這個變量只支持同時只有一個線程能把它修改成1,因此它修改完了必定要讓其它線程可見,所以,這個變量須要使用volatile來修飾。

private volatile int state;

CAS

這個變量的修改必須是原子操做,因此咱們須要CAS更新它,咱們這裏使用Unsafe來直接CAS更新int類型的state。

固然,這個變量若是直接使用AtomicInteger也是能夠的,不過,既然咱們學習了更底層的Unsafe類那就應該用(浪)起來。

private boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

一個隊列

隊列的實現有不少,數組、鏈表均可以,咱們這裏採用鏈表,畢竟鏈表實現隊列相對簡單一些,不用考慮擴容等問題。

這個隊列的操做頗有特色:

放元素的時候都是放到尾部,且多是多個線程一塊兒放,因此對尾部的操做要CAS更新;

喚醒一個元素的時候從頭部開始,但同時只有一個線程在操做,即得到了鎖的那個線程,因此對頭部的操做不須要CAS去更新。

private static class Node {
    // 存儲的元素爲線程
    Thread thread;
    // 前一個節點(能夠沒有,但實現起來很困難)
    Node prev;
    // 後一個節點
    Node next;

    public Node() {
    }

    public Node(Thread thread, Node prev) {
        this.thread = thread;
        this.prev = prev;
    }
}
// 鏈表頭
private volatile Node head;
// 鏈表尾
private volatile Node tail;
// 原子更新tail字段
private boolean compareAndSetTail(Node expect, Node update) {
    return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}

這個隊列很簡單,存儲的元素是線程,須要有指向下一個待喚醒的節點,前一個節點無關緊要,可是沒有實現起來很困難,不信學完這篇文章你試試。

加鎖

public void lock() {
    // 嘗試更新state字段,更新成功說明佔有了鎖
    if (compareAndSetState(0, 1)) {
        return;
    }
    // 未更新成功則入隊
    Node node = enqueue();
    Node prev = node.prev;
    // 再次嘗試獲取鎖,須要檢測上一個節點是否是head,按入隊順序加鎖
    while (node.prev != head || !compareAndSetState(0, 1)) {
        // 未獲取到鎖,阻塞
        unsafe.park(false, 0L);
    }
    // 下面不須要原子更新,由於同時只有一個線程訪問到這裏
    // 獲取到鎖了且上一個節點是head
    // head後移一位
    head = node;
    // 清空當前節點的內容,協助GC
    node.thread = null;
    // 將上一個節點從鏈表中剔除,協助GC
    node.prev = null;
    prev.next = null;
}
// 入隊
private Node enqueue() {
    while (true) {
        // 獲取尾節點
        Node t = tail;
        // 構造新節點
        Node node = new Node(Thread.currentThread(), t);
        // 不斷嘗試原子更新尾節點
        if (compareAndSetTail(t, node)) {
            // 更新尾節點成功了,讓原尾節點的next指針指向當前節點
            t.next = node;
            return node;
        }
    }
}

(1)嘗試獲取鎖,成功了就直接返回;

(2)未獲取到鎖,就進入隊列排隊;

(3)入隊以後,再次嘗試獲取鎖;

(4)若是不成功,就阻塞;

(5)若是成功了,就把頭節點後移一位,並清空當前節點的內容,且與上一個節點斷絕關係;

(6)加鎖結束;

解鎖

// 解鎖
public void unlock() {
    // 把state更新成0,這裏不須要原子更新,由於同時只有一個線程訪問到這裏
    state = 0;
    // 下一個待喚醒的節點
    Node next = head.next;
    // 下一個節點不爲空,就喚醒它
    if (next != null) {
        unsafe.unpark(next.thread);
    }
}

(1)把state改爲0,這裏不須要CAS更新,由於如今還在加鎖中,只有一個線程去更新,在這句以後就釋放了鎖;

(2)若是有下一個節點就喚醒它;

(3)喚醒以後就會接着走上面lock()方法的while循環再去嘗試獲取鎖;

(4)喚醒的線程不是百分之百能獲取到鎖的,由於這裏state更新成0的時候就解鎖了,以後可能就有線程去嘗試加鎖了。

測試

上面完整的鎖的實現就完了,是否是很簡單,可是它是否是真的可靠呢,敢不敢來試試?!

直接上測試代碼:

private static int count = 0;

public static void main(String[] args) throws InterruptedException {
    MyLock lock = new MyLock();

    CountDownLatch countDownLatch = new CountDownLatch(1000);

    IntStream.range(0, 1000).forEach(i -> new Thread(() -> {
        lock.lock();

        try {
            IntStream.range(0, 10000).forEach(j -> {
                count++;
            });
        } finally {
            lock.unlock();
        }
//            System.out.println(Thread.currentThread().getName());
        countDownLatch.countDown();
    }, "tt-" + i).start());

    countDownLatch.await();

    System.out.println(count);
}

運行這段代碼的結果是老是打印出10000000(一千萬),說明咱們的鎖是正確的、可靠的、完美的。

總結

(1)本身動手寫一個鎖須要作準備:一個變量、一個隊列、Unsafe類。

(2)原子更新變量爲1說明得到鎖成功;

(3)原子更新變量爲1失敗說明得到鎖失敗,進入隊列排隊;

(4)更新隊列尾節點的時候是多線程競爭的,因此要使用原子更新;

(5)更新隊列頭節點的時候只有一個線程,不存在競爭,因此不須要使用原子更新;

(6)隊列節點中的前一個節點prev的使用很巧妙,沒有它將很難實現一個鎖,只有寫過的人才明白,不信你試試^^

彩蛋

(1)咱們實現的鎖支持可重入嗎?

答:不可重入,由於咱們每次只把state更新爲1。若是要支持可重入也很簡單,獲取鎖時檢測鎖是否是被當前線程佔有着,若是是就把state的值加1,釋放鎖時每次減1便可,減爲0時表示鎖已釋放。

(2)咱們實現的鎖是公平鎖仍是非公平鎖?

答:非公平鎖,由於獲取鎖的時候咱們先嚐試了一次,這裏並非嚴格的排隊,因此是非公平鎖。

(3)完整源碼

關注個人公衆號「彤哥讀源碼」,後臺回覆「mylock」獲取本章完整源碼。

注:下一章咱們將開始分析傳說中的AQS,這章是基礎,請各位老鐵務必搞明白。

推薦閱讀

  1. 死磕 java魔法類之Unsafe解析

  2. 死磕 java同步系列之JMM(Java Memory Model)

  3. 死磕 java同步系列之volatile解析

  4. 死磕 java同步系列之synchronized解析


歡迎關注個人公衆號「彤哥讀源碼」,查看更多源碼系列文章, 與彤哥一塊兒暢遊源碼的海洋。

qrcode

相關文章
相關標籤/搜索