ReentrantLock實現原理及源碼分析

来源:http://www.cnblogs.com/chengxiao/archive/2017/07/29/7255941.html
-Advertisement-
Play Games

ReentrantLock是Java併發包中提供的一個可重入的互斥鎖。ReentrantLock和synchronized在基本用法,行為語義上都是類似的,同樣都具有可重入性。只不過相比原生的Synchronized,ReentrantLock增加了一些高級的擴展功能,比如它可以實現公平鎖,同時也可 ...


  ReentrantLock是Java併發包中提供的一個可重入的互斥鎖ReentrantLocksynchronized在基本用法,行為語義上都是類似的,同樣都具有可重入性。只不過相比原生的Synchronized,ReentrantLock增加了一些高級的擴展功能,比如它可以實現公平鎖,同時也可以綁定多個Conditon

可重入性/公平鎖/非公平鎖

  可重入性

    所謂的可重入性,就是可以支持一個線程對鎖的重覆獲取,原生的synchronized就具有可重入性,一個用synchronized修飾的遞歸方法,當線程在執行期間,它是可以反覆獲取到鎖的,而不會出現自己把自己鎖死的情況。ReentrantLock也是如此,在調用lock()方法時,已經獲取到鎖的線程,能夠再次調用lock()方法獲取鎖而不被阻塞。那麼有可重入鎖,就有不可重入鎖,我們在之前文章中自定義的一個Mutex鎖就是個不可重入鎖,不過使用場景極少而已。

  公平鎖/非公平鎖

    所謂公平鎖,顧名思義,意指鎖的獲取策略相對公平,當多個線程在獲取同一個鎖時,必須按照鎖的申請時間來依次獲得鎖,排排隊,不能插隊;非公平鎖則不同,當鎖被釋放時,等待中的線程均有機會獲得鎖。synchronized是非公平鎖,ReentrantLock預設也是非公平的,但是可以通過帶boolean參數的構造方法指定使用公平鎖,但非公平鎖的性能一般要優於公平鎖。

  synchronized是Java原生的互斥同步鎖,使用方便,對於synchronized修飾的方法或同步塊,無需再顯式釋放鎖。synchronized底層是通過monitorenter和monitorexit兩個位元組碼指令來實現加鎖解鎖操作的。而ReentrantLock做為API層面的互斥鎖,需要顯式地去加鎖解鎖。 

class X {
    private final ReentrantLock lock = new ReentrantLock();
    // ...
 
    public void m() {
      lock.lock();  // 加鎖
      try {
        // ... 函數主題
      } finally {
        lock.unlock() //解鎖
      }
    }
  }

源碼分析

  接下來我們從源碼角度來看看ReentrantLock的實現原理,它是如何保證可重入性,又是如何實現公平鎖的。

  ReentrantLock是基於AQS的,AQS是Java併發包中眾多同步組件的構建基礎,它通過一個int類型的狀態變數state和一個FIFO隊列來完成共用資源的獲取,線程的排隊等待等。AQS是個底層框架,採用模板方法模式,它定義了通用的較為複雜的邏輯骨架,比如線程的排隊,阻塞,喚醒等,將這些複雜但實質通用的部分抽取出來,這些都是需要構建同步組件的使用者無需關心的,使用者僅需重寫一些簡單的指定的方法即可(其實就是對於共用變數state的一些簡單的獲取釋放的操作)。

  上面簡單介紹了下AQS,詳細內容可參考本人的另一篇文章《Java併發包基石-AQS詳解》,此處就不再贅述了。先來看常用的幾個方法,我們從上往下推。

  無參構造器(預設為公平鎖)

public ReentrantLock() {
        sync = new NonfairSync();//預設是非公平的
    }

  sync是ReentrantLock內部實現的一個同步組件,它是Reentrantlock的一個靜態內部類,繼承於AQS,後面我們再分析。

  帶布爾值的構造器(是否公平)

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();//fair為true,公平鎖;反之,非公平鎖
    }

  看到了吧,此處可以指定是否採用公平鎖,FailSync和NonFailSync亦為Reentrantlock的靜態內部類,都繼承於Sync

  再來看看幾個我們常用到的方法

  lock()

public void lock() {
        sync.lock();//代理到Sync的lock方法上
    }

  Sync的lock方法是抽象的,實際的lock會代理到FairSync或是NonFairSync上(根據用戶的選擇來決定,公平鎖還是非公平鎖)

  lockInterruptibly()

public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);//代理到sync的相應方法上,同lock方法的區別是此方法響應中斷
    }

  此方法響應中斷,當線程在阻塞中的時候,若被中斷,會拋出InterruptedException異常 

  tryLock()

public boolean tryLock() {
        return sync.nonfairTryAcquire(1);//代理到sync的相應方法上
    }

  tryLock,嘗試獲取鎖,成功則直接返回true,不成功也不耽擱時間,立即返回false。

  unlock()

public void unlock() {
        sync.release(1);//釋放鎖
    }

  釋放鎖,調用sync的release方法,其實是AQS的release邏輯。

   newCondition()

    獲取一個conditon,ReentrantLock支持多個Condition

public Condition newCondition() {
        return sync.newCondition();
    }

  其他方法就不再贅述了,若想繼續瞭解可去API中查看。

  小結

  其實從上面這寫方法的介紹,我們都能大概梳理出ReentrantLock的處理邏輯,其內部定義了三個重要的靜態內部類,Sync,NonFairSync,FairSync。Sync作為ReentrantLock中公用的同步組件,繼承了AQS(要利用AQS複雜的頂層邏輯嘛,線程排隊,阻塞,喚醒等等);NonFairSync和FairSync則都繼承Sync,調用Sync的公用邏輯,然後再在各自內部完成自己特定的邏輯(公平或非公平)。

  接下來,關於如何實現重入性,如何實現公平性,就得去看這幾個靜態內部類了

  NonFairSync(非公平可重入鎖)

static final class NonfairSync extends Sync {//繼承Sync
        private static final long serialVersionUID = 7316153563782823691L;
        /** 獲取鎖 */
        final void lock() {
            if (compareAndSetState(0, 1))//CAS設置state狀態,若原值是0,將其置為1
                setExclusiveOwnerThread(Thread.currentThread());//將當前線程標記為已持有鎖
            else
                acquire(1);//若設置失敗,調用AQS的acquire方法,acquire又會調用我們下麵重寫的tryAcquire方法。這裡說的調用失敗有兩種情況:1當前沒有線程獲取到資源,state為0,但是將state由0設置為1的時候,其他線程搶占資源,將state修改了,導致了CAS失敗;2 state原本就不為0,也就是已經有線程獲取到資源了,有可能是別的線程獲取到資源,也有可能是當前線程獲取的,這時線程又重覆去獲取,所以去tryAcquire中的nonfairTryAcquire我們應該就能看到可重入的實現邏輯了。
        }
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);//調用Sync中的方法
        }
    }   

  nonfairTryAcquire()

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//獲取當前線程
            int c = getState();//獲取當前state值
            if (c == 0) {//若state為0,意味著沒有線程獲取到資源,CAS將state設置為1,並將當前線程標記我獲取到排他鎖的線程,返回true
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//若state不為0,但是持有鎖的線程是當前線程
                int nextc = c + acquires;//state累加1
                if (nextc < 0) // int類型溢出了
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);//設置state,此時state大於1,代表著一個線程多次獲鎖,state的值即是線程重入的次數
                return true;//返回true,獲取鎖成功
            }
            return false;//獲取鎖失敗了
        }

  簡單總結下流程:

    1.先獲取state值,若為0,意味著此時沒有線程獲取到資源,CAS將其設置為1,設置成功則代表獲取到排他鎖了;

    2.若state大於0,肯定有線程已經搶占到資源了,此時再去判斷是否就是自己搶占的,是的話,state累加,返回true,重入成功,state的值即是線程重入的次數;

    3.其他情況,則獲取鎖失敗。

   來看看可重入公平鎖的處理邏輯

  FairSync

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);//直接調用AQS的模板方法acquire,acquire會調用下麵我們重寫的這個tryAcquire
        }

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//獲取當前線程
            int c = getState();//獲取state值
            if (c == 0) {//若state為0,意味著當前沒有線程獲取到資源,那就可以直接獲取資源了嗎?NO!這不就跟之前的非公平鎖的邏輯一樣了嘛。看下麵的邏輯
                if (!hasQueuedPredecessors() &&//判斷在時間順序上,是否有申請鎖排在自己之前的線程,若沒有,才能去獲取,CAS設置state,並標記當前線程為持有排他鎖的線程;反之,不能獲取!這即是公平的處理方式。
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//重入的處理邏輯,與上文一致,不再贅述
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

  可以看到,公平鎖的大致邏輯與非公平鎖是一致的,不同的地方在於有了!hasQueuedPredecessors()這個判斷邏輯,即便state為0,也不能貿然直接去獲取,要先去看有沒有還在排隊的線程,若沒有,才能嘗試去獲取,做後面的處理。反之,返回false,獲取失敗。

  看看這個判斷是否有排隊中線程的邏輯

  hasQueuedPredecessors()

    public final boolean hasQueuedPredecessors() {
        Node t = tail; // 尾結點
        Node h = head;//頭結點
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());//判斷是否有排在自己之前的線程
    }

 需要註意的是,這個判斷是否有排在自己之前的線程的邏輯稍微有些繞,我們來梳理下,由代碼得知,有兩種情況會返回true,我們將此邏輯分解一下(註意:返回true意味著有其他線程申請鎖比自己早,需要放棄搶占)

  1. h !=t && (s = h.next) == null,這個邏輯成立的一種可能是head指向頭結點,tail此時還為null。考慮這種情況:當其他某個線程去獲取鎖失敗,需構造一個結點加入同步隊列中(假設此時同步隊列為空),在添加的時候,需要先創建一個無意義傀儡頭結點(在AQS的enq方法中,這是個自旋CAS操作),有可能在將head指向此傀儡結點完畢之後,還未將tail指向此結點。很明顯,此線程時間上優於當前線程,所以,返回true,表示有等待中的線程且比自己來的還早。

  2.h != t && (s = h.next) != null && s.thread != Thread.currentThread()。同步隊列中已經有若幹排隊線程且當前線程不是隊列的老二結點,此種情況會返回true。假如沒有s.thread !=Thread.currentThread()這個判斷的話,會怎麼樣呢?若當前線程已經在同步隊列中是老二結點(頭結點此時是個無意義的傀儡結點),此時持有鎖的線程釋放了資源,喚醒老二結點線程,老二結點線程重新tryAcquire(此邏輯在AQS中的acquireQueued方法中),又會調用到hasQueuedPredecessors,不加s.thread !=Thread.currentThread()這個判斷的話,返回值就為true,導致tryAcquire失敗。

  最後,來看看ReentrantLock的tryRelease,定義在Sync中

 protected final boolean tryRelease(int releases) {
            int c = getState() - releases;//減去1個資源
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //若state值為0,表示當前線程已完全釋放乾凈,返回true,上層的AQS會意識到資源已空出。若不為0,則表示線程還占有資源,只不過將此次重入的資源的釋放了而已,返回false。
            if (c == 0) {
                free = true;//
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

 總結

  ReentrantLock是一種可重入的,可實現公平性的互斥鎖,它的設計基於AQS框架,可重入和公平性的實現邏輯都不難理解,每重入一次,state就加1,當然在釋放的時候,也得一層一層釋放。至於公平性,在嘗試獲取鎖的時候多了一個判斷:是否有比自己申請早的線程在同步隊列中等待,若有,去等待;若沒有,才允許去搶占。

  

  


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

-Advertisement-
Play Games
更多相關文章
  • 1 class Program 2 { 3 //數組是引用類型 4 //如果把數組或類等其他引用類型傳遞給方法,對應的方法就會使用該引用類型改編數組中值, 5 //而新值會反射到原始數組上 6 static void SomeFunction(int[] ints, int i) 7 { 8 int ...
  • 1、前言 surging受到不少.net同學的青睞,也提了不少問題,提的最多的是什麼時候集成API 網關,在這裡回答大家最近已經開始著手研發,應該在1,2個月內會有個初版API網關,其它像Token身份驗證,限流降級等功能完成時間會往後推 最近也更新了surging新的版本 更新內容: 1. Cac ...
  • 一.什麼是ORM 對象關係映射(Object Relational Mapping,簡稱ORM)模式是一種為瞭解決面向對象與關係資料庫存在的互不匹配的現象的技術。 簡單來說,ORM 是通過使用描述對象和資料庫之間映射的元數據,將程式中的對象自動持久化到關係資料庫中或者將資料庫的數據拉取出來 二.EF ...
  • 前言 工作中經常會寫一些重覆的代碼片段,如自動屬性、for迴圈、Action等等,針對這種情況,VisualStudio已經給我們提供了一個非常方便的功能——代碼片段,是我們可以簡單的輸入幾個字母就能生成大段代碼。 但是,工作中總會遇到一些重覆代碼是VisualStudio沒有提供的,這時就需要我們 ...
  • 反射 通過字元串映射或修改程式運行時的狀態、屬性、方法, 有以下4個方法 1、getattr(object, name[, default]) -> value Get a named attribute from an object; getattr(x, 'y') is equivalent t ...
  • 寫這篇文章的目的是想總結一下自己這麼多年來使用java的一些心得體會,主要是和一些java基礎知識點相關的,所以也希望能分享給剛剛入門的Java程式員和打算入Java開發這個行當的準新手們,希望可以給大家一些經驗,能讓大家更好學習和使用Java。 這次介紹的主要內容是和J2SE相關的部分,另外,會在 ...
  • 類的特殊成員方法 1. __doc__ 表示類的描述信息 __doc__是用來列印類的描述信息。就是類的註釋。 2.__module__和__class__ __module__表示當前操作的對象在那個模塊 __class__ 表示當前操作的對象的類是什麼 3. __init__ 構造方法,通過類創 ...
  • 15套java架構師、集群、高可用、高可擴 展、高性能、高併發、性能優化、Spring boot、Redis、ActiveMQ、Nginx、Mycat、Netty、Jvm大型分佈 式項目實戰視頻教程 視頻課程包含: 高級Java架構師包含:Spring boot、Spring cloud、Dubbo ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...