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

来源:https://www.cnblogs.com/tong-yuan/archive/2019/05/23/mylock.html
-Advertisement-
Play Games

自己動手寫一個鎖需要哪些知識? 自己動手寫一個鎖到底有多簡單? 自己能不能寫出來一個完美的鎖? ...


問題

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

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

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

簡介

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

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

分析

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

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

但是,我們自己是無法修改對象頭信息的,那麼我們可不可以用一個變數來代替呢?

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

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

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

最後,這些線程排隊的時候幹嘛呢?它們不能再繼續執行自己的程式,那就只能阻塞了,阻塞完了當輪到這個線程的時候還要喚醒,所以我們還需要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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1.二維矩陣的轉置 2.矩陣相加,A,B矩陣均需要為一個N*M的矩陣,即相加矩陣的行和列必須相等 3.矩陣相乘,A,B矩陣需要滿足條件為A為m*n的矩陣,B為n*p的矩陣,結果C為m*p的矩陣 4.編寫函數利用三項式壓縮稀疏矩陣稀疏矩陣:一個矩陣的大部分元素為0,則是稀疏矩陣 三項式:非零項用(i, ...
  • 所屬網站分類: 面試經典 > python 作者:外星人入侵 鏈接: http://www.pythonheidong.com/blog/article/13/ 來源:python黑洞網 www.pythonheidong.com webbrowser模塊提供了一個高級介面來顯示基於Web的文檔,大 ...
  • 積跬步,行千里,先從最簡單的開始寫。 這一篇介紹V8中的時間模塊,與libuv粗糙的update_loop_time方法不同,V8有一套獨立完整的類負責管理時間。 該類位於src/base/platform/time.h,是一個輔助模塊,首先來看一下繼承樹。 整個模塊的繼承關係比較簡單,一般常用的就 ...
  • 統計字元串中每一個不同的字元 JDK9的新特性: List介面,Set介面,Map介面裡邊增加了一個靜態方法of,可以給集合一次性添加多個元素 使用前提:當集合中存儲的元素的個數已經確定了,不再改變使用,也就是說,添加完元素之後,就不能再使用put方法來添加元素了 註意事項: 1. of方法只適用於 ...
  • 伺服器免密登錄 由於有多台伺服器,每次登錄還需要 去找對應的伺服器地址,然後輸入密碼,為了避免麻煩,就使用了免密登錄。普通登錄方式: ssh -p 22 [email protected] 每次登錄還需要輸入密碼,比較麻煩 更換免密碼登錄: 本地操作: 本地的公鑰位置: ~/.ssh/i... ...
  • 今天工作的時候接觸到客戶的一臺伺服器,業務邏輯比較簡單 。估算pv在120w左右吧,用的是阿裡雲2c4g的伺服器。一大早就開始卡頓了,登陸伺服器後查看負載到了八九十。 之後就想辦法調整一下吧。突然想起某位前輩說過的:開啟opcache吧,真的會變快的。 於是我馬上就開始整,過程很簡單 1.進入php ...
  • 對於Java初學者,可能會面對這麼一個問題,Java環境的配置,那麼廢話少說,直接開始。首先找到jdk的安裝包(我這邊以jdk1.8為例),雙擊安裝。 到這邊我們的jdk已經成功安裝,但這樣我們有些同學的eclipse依然沒法使用,那是因為eclipse需要配置jdk才能正常使用,下麵開始配置jdk ...
  • 1,函數重寫回顧: 1,父類中被重寫的函數依然會繼承給子類; 2,子類中重寫的函數將覆蓋父類中的函數; 1,重寫父類當中提供的函數是因為父類當中提供的這個函數版本不能滿足我們的需求,因此我們要重寫; 2,期望只要是子類對象,則調用子類當中的版本,而不是父類當中定義的函數版本; 3,通過作用域分辨符( ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...