1. 程式人生 > >死磕 java同步系列之自己動手寫一個鎖Lock

死磕 java同步系列之自己動手寫一個鎖Lock

問題

(1)自己動手寫一個鎖需要哪些知識?

(2)自己動手寫一個鎖到底有多簡單?

(3)自己能不能寫出來一個完美的鎖?

簡介

本篇文章的目標一是自己動手寫一個鎖,這個鎖的功能很簡單,能進行正常的加鎖、解鎖操作。

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

分析

自己動手寫一個鎖需要準備些什麼呢?

首先,在上一章學習synchronized的時候我們說過它的實現原理是更改物件頭中的MarkWord,標記為已加鎖或未加鎖。

但是,我們自己是無法修改物件頭資訊的,那麼我們可不可以用一個變數來代替呢?

比如,這個變數的值為1的時候就說明已加鎖,變數值為0的時候就說明未加鎖,我覺得可行。

其次,我們要保證多個執行緒對上面我們定義的變數的爭用是可控的,所謂可控即同時只能有一個執行緒把它的值修改為1,且當它的值為1的時候其它執行緒不能再修改它的值,這種是不是就是典型的CAS操作,所以我們需要使用Unsafe這個類來做CAS操作。

然後,我們知道在多執行緒的環境下,多個執行緒對同一個鎖的爭用肯定只有一個能成功,那麼,其它的執行緒就要排隊,所以我們還需要一個佇列。

最後,這些執行緒排隊的時候幹嘛呢?它們不能再繼續執行自己的程式,那就只能阻塞了,阻塞完了當輪到這個執行緒的時候還要喚醒,所以我們還需要Unsfae這個類來阻塞(park)和喚醒(unpark)執行緒。

基於以上四點,我們需要的神器大致有:一個變數、一個佇列、執行CAS/park/unpark的Unsafe類。

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

關於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解析


歡迎關注我的公眾號“彤哥讀原始碼”,檢視更多原始碼系列文章, 與彤哥一起暢遊原始碼的海洋。

相關推薦

java同步系列自己動手一個Lock

問題 (1)自己動手寫一個鎖需要哪些知識? (2)自己動手寫一個鎖到底有多簡單? (3)自己能不能寫出來一個完美的鎖? 簡介 本篇文章的目標一是自己動手寫一個鎖,這個鎖的功能很簡單,能進行正常的加鎖、解鎖操作。 本篇文章的目標二是通過自己動手寫一個鎖,能更好地理解後面章節將要學習的AQS及各種同步器實現的原理

java執行緒系列自己動手一個執行緒池

歡迎關注我的公眾號“彤哥讀原始碼”,檢視更多原始碼系列文章, 與彤哥一起暢遊原始碼的海洋。 (手機橫屏看原始碼更方便) 問題 (1)自己動手寫一個執行緒池需要考慮哪些因素? (2)自己動手寫的執行緒池如何測試? 簡介 執行緒池是Java併發程式設計中經常使用到的技術,那麼自己如何動手寫一個執行緒池呢?本

java執行緒系列自己動手一個執行緒池(續)

(手機橫屏看原始碼更方便) 問題 (1)自己動手寫的執行緒池如何支援帶返回值的任務呢? (2)如果任務執行的過程中丟擲異常了該

java同步系列開篇

討論 關註 使用 避免死鎖 更新數據 讀寫 上下文切換 monit 缺點 簡介 同步系列,這是彤哥想了好久的名字,本來是準備寫鎖相關的內容,但是java中的CountDownLatch、Semaphore、CyclicBarrier這些類又不屬於鎖,它們和鎖又有很多共同點,

java同步系列JMM(Java Memory Model)

簡介 Java記憶體模型是在硬體記憶體模型上的更高層的抽象,它遮蔽了各種硬體和作業系統訪問的差異性,保證了Java程式在各種平臺下對記憶體的訪問都能達到一致的效果。 硬體記憶體模型 在正式講解Java的記憶體模型之前,我們有必要先了解一下硬體層面的一些東西。 在現代計算機的硬體體系中,CPU的運算速度是非常快

java同步系列volatile解析

問題 (1)volatile是如何保證可見性的? (2)volatile是如何禁止重排序的? (3)volatile的實現原理? (4)volatile的缺陷? 簡介 volatile可以說是Java虛擬機器提供的最輕量級的同步機制了,但是它並不容易被正確地理解,以至於很多人不習慣使用它,遇到多執行緒問題一律

java同步系列synchronized解析

問題 (1)synchronized的特性? (2)synchronized的實現原理? (3)synchronized是否可重入? (4)synchronized是否是公平鎖? (5)synchronized的優化? (6)synchronized的五種使用方式? 簡介 synchronized關鍵字是Ja

java同步系列AQS起篇

問題 (1)AQS是什麼? (2)AQS的定位? (3)AQS的實現原理? (4)基於AQS實現自己的鎖? 簡介 AQS的全稱是AbstractQueuedSynchronizer,它的定位是為Java中幾乎所有的鎖和同步器提供一個基礎框架。 AQS是基於FIFO的佇列實現的,並且內部維護了一個狀態變數sta

java同步系列ReentrantLock原始碼解析(一)——公平、非公平

問題 (1)重入鎖是什麼? (2)ReentrantLock如何實現重入鎖? (3)ReentrantLock為什麼預設是非公平模式? (4)ReentrantLock除了可重入還有哪些特性? 簡介 Reentrant = Re + entrant,Re是重複、又、再的意思,entrant是enter的名詞或

java同步系列ReentrantLock原始碼解析(二)——條件

問題 (1)條件鎖是什麼? (2)條件鎖適用於什麼場景? (3)條件鎖的await()是在其它執行緒signal()的時候喚醒的嗎? 簡介 條件鎖,是指在獲取鎖之後發現當前業務場景自己無法處理,而需要等待某個條件的出現才可以繼續處理時使用的一種鎖。 比如,在阻塞佇列中,當佇列中沒有元素的時候是無法彈出一個元素

java同步系列ReentrantLock VS synchronized——結果可能跟你想的不一樣

問題 (1)ReentrantLock有哪些優點? (2)ReentrantLock有哪些缺點? (3)ReentrantLock

java同步系列ReentrantReadWriteLock原始碼解析

問題 (1)讀寫鎖是什麼? (2)讀寫鎖具有哪些特性? (3)ReentrantReadWriteLock是怎麼實現讀寫鎖的? (4)如何使用ReentrantReadWriteLock實現高效安全的TreeMap? 簡介 讀寫鎖是一種特殊的鎖,它把對共享資源的訪問分為讀訪問和寫訪問,多個執行緒可以同時對共享

java同步系列Semaphore原始碼解析

問題 (1)Semaphore是什麼? (2)Semaphore具有哪些特性? (3)Semaphore通常使用在什麼場景中? (

java同步系列AQS終篇(面試)

問題 (1)AQS的定位? (2)AQS的重要組成部分? (3)AQS運用的設計模式? (4)AQS的總體流程? 簡介 AQS的全稱是AbstractQueuedSynchronizer,它的定位是為Java中幾乎所有的鎖和同步器提供一個基礎框架。 在之前的章節中,我們一起學習了ReentrantLock、R

java同步系列StampedLock原始碼解析

問題 (1)StampedLock是什麼? (2)StampedLock具有什麼特性? (3)StampedLock是否支援可重入

java同步系列CyclicBarrier原始碼解析——有圖有真相

問題 (1)CyclicBarrier是什麼? (2)CyclicBarrier具有什麼特性? (3)CyclicBarrier與

java同步系列Phaser原始碼解析

問題 (1)Phaser是什麼? (2)Phaser具有哪些特性? (3)Phaser相對於CyclicBarrier和Count

java同步系列mysql分散式

問題 (1)什麼是分散式鎖? (2)為什麼需要分散式鎖? (3)mysql如何實現分散式鎖? (4)mysql分散式鎖的優點和缺點? 簡介 隨著併發量的不斷增加,單機的服務遲早要向多節點或者微服務進化,這時候原來單機模式下使用的synchronized或者ReentrantLock將不再適用,我們迫切地需要一

java同步系列zookeeper分散式

(2)zookeeper分散式鎖有哪些優點? (3)zookeeper分散式鎖有哪些缺點? 簡介 zooKeeper是一個分散式的,開放原始碼的分散式應用程式協調服務,它可以為分散式應用提供一致性服務,它是Hadoop和Hbase的重要元件,同時也可以作為配置中心、註冊中心運用在微服務體系中。 本章我們將介