Java併發編程-ReentrantLock

来源:https://www.cnblogs.com/iou123lg/archive/2018/08/25/9535710.html
-Advertisement-
Play Games

上文已經總結了AQS的前世今生,有了這個基礎我們就可以來進一步學習併發工具類。首先我們要學習的就是ReentrantLock,本文將從ReentrantLock的產生背景、源碼原理解析和應用來學習ReentrantLock這個併發工具類。 1、 產生背景 前面我們已經學習過了synchronized ...


  上文已經總結了AQS的前世今生,有了這個基礎我們就可以來進一步學習併發工具類。首先我們要學習的就是ReentrantLock,本文將從ReentrantLock的產生背景、源碼原理解析和應用來學習ReentrantLock這個併發工具類。

1、 產生背景

  前面我們已經學習過了synchronized,這個關鍵字可以確保對象在併發訪問中的原子性、可見性和有序性,這個關鍵字的底層交由了JVM通過C++來實現,既然是JVM實現,就依賴於JVM,程式員就無法在Java層面進行擴展和優化,肯定就靈活性不高,比如程式員在使用時就無法中斷一個正在等待獲取鎖的線程,或者無法在請求一個鎖時無限的等待下去。基於這樣一個背景,Doug Lea構建了一個在記憶體語義上和synchronized一樣效果的Java類,同時還擴展了其他一些高級特性,比如定時的鎖等待、可中斷的鎖等待和公平性等,這個類就是ReentrantLock。

2、 源碼原理解析

2.1 可重入性原理

  在synchronized一文中,我們認為synchronized是一種重量級鎖,它的實現對應的是C++的ObjectMonitor,代碼如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //記錄線程獲取鎖的次數
    _waiters      = 0;
    _recursions   = 0; //鎖的重入次數
    _object       = NULL;
    _owner        = NULL;//指向持有ObjectMonitor對象的線程
    _WaitSet      = NULL; //等待條件隊列 類似AQS的ConditionObject
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //同步隊列 類似AQS的CLH隊列
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

  從代碼中可以看到synchronized實現的鎖的重入依賴於JVM,JVM為每個對象的鎖關聯一個計數器_count和一個所有者線程_owner,當計數器為0的時候就認為鎖沒有被任何線程持有,當線程請求一個未被持有的鎖時,JVM就記下鎖的持有者,並將計數器的值設置為1,如果是同一個線程再次獲取這個鎖,計數器的值遞增,而當線程退出時,計數器的值遞減,直到計數器為0時,鎖被釋放。

  ReentrantLock實現了在記憶體語義上的synchronized,固然也是支持可重入的,那麼ReentrantLock是如何支持的呢,讓我們以非公平鎖的實現看下ReentrantLock的可重入,代碼如下:

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//當前線程
            int c = getState();
            if (c == 0) {//表示鎖未被搶占
                if (compareAndSetState(0, acquires)) {//獲取到同步狀態
                    setExclusiveOwnerThread(current); //當前線程占有鎖
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//線程已經占有鎖了 重入
                int nextc = c + acquires;//同步狀態記錄重入的次數
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases; //既然可重入 就需要釋放重入獲取的鎖
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;//只有線程全部釋放才返回true
                setExclusiveOwnerThread(null); //同步隊列的線程都可以去獲取同步狀態了
            }
            setState(c); 
            return free;
        }

  看到這也就明白了上文說的ReentrantLock類使用AQS同步狀態來保存鎖重覆持有的次數。當鎖被一個線程獲取時,ReentrantLock也會記錄下當前獲得鎖的線程標識,以便檢查是否是重覆獲取,以及當錯誤的線程試圖進行解鎖操作時檢測是否存在非法狀態異常。

2.2 獲取和釋放鎖

  如下是獲取和釋放鎖的方法:

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

  獲取鎖的時候依賴的是內部類Sync的lock()方法,該方法又有2個實現類方法,分別是非公平鎖NonfairSync和公平鎖FairSync,具體咱們下一小節分析。再來看下釋放鎖,釋放鎖的時候實際調用的是AQS的release方法,代碼如下:

public final boolean release(int arg) {
        if (tryRelease(arg)) {//調用子類的tryRelease 實際就是Sync的tryRelease
            Node h = head;//取同步隊列的頭節點
            if (h != null && h.waitStatus != 0)//同步隊列頭節點不為空且不是初始狀態
                unparkSuccessor(h);//釋放頭節點 喚醒後續節點
            return true;
        }
        return false;
}

  Sync的tryRelease就是上一小節的重入釋放方法,如果是同一個線程,那麼鎖的重入次數就依次遞減,直到重入次數為0,此方法才會返回true,此時斷開頭節點喚醒後續節點去獲取AQS的同步狀態。

2.3 公平鎖和非公平鎖

  公平鎖還是非公平鎖取決於ReentrantLock的構造方法,預設無參構造方法是NonfairSync,含參構造方法,入參true為FairSync,入參false為NonfairSync。

public ReentrantLock() {
   sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
   sync = fair ? new FairSync() : new NonfairSync();
}

  再分別來看看非公平鎖和公平鎖的實現。

static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))//通過CAS來獲取同步狀態 也就是鎖
                setExclusiveOwnerThread(Thread.currentThread());//獲取成功線程占有鎖
            else
                acquire(1);//獲取失敗 進入AQS同步隊列排隊等待 執行AQS的acquire方法 
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

  在AQS的acquire方法中先調用子類tryAcquire,也就是nonfairTryAcquire,見2.1小節。可以看出非公平鎖中,搶到AQS的同步狀態的未必是同步隊列的首節點,只要線程通過CAS搶到了同步狀態或者在acquire中搶到同步狀態,就優先占有鎖,而相對同步隊列這個嚴格的FIFO隊列來說,所以會被認為是非公平鎖。

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

        final void lock() {
            acquire(1);//嚴格按照AQS的同步隊列要求去獲取同步狀態
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//獲取當前線程
            int c = getState();
            if (c == 0) {//鎖未被搶占
                if (!hasQueuedPredecessors() &&//沒有前驅節點
                    compareAndSetState(0, acquires)) {//CAS獲取同步狀態
                    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;
        }
    }

  公平鎖的實現直接調用AQS的acquire方法,acquire中調用tryAcquire。和非公平鎖相比,這裡不會執行一次CAS,接下來在tryAcquire去搶占鎖的時候,也會先調用hasQueuedPredecessors看看前面是否有節點已經在等待獲取鎖了,如果存在則同步隊列的前驅節點優先。

public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        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());//頭節點的後續節點為空或者不是當前線程
    }

  雖然公平鎖看起來在公平性上比非公平鎖好,但是公平鎖為此付出了大量線程切換的代價,而非公平鎖在鎖的獲取上不能保證公平,就有可能出現鎖饑餓,即有的線程多次獲取鎖而有的線程獲取不到鎖,沒有大量的線程切換保證了非公平鎖的吞吐量。

3、 應用

3.1普通的線程鎖

  標準形式如下:

ReentrantLock lock = new ReentrantLock();
try {
        lock.lock();
     //……
 }finally {
     lock.unlock();
 }

  這種用法和synchronized效果是一樣的,但是必須顯示的聲明lock和unlock。

3.2 帶限制的鎖

public boolean tryLock()// 嘗試獲取鎖,立即返回獲取結果 輪詢鎖
public boolean tryLock(long timeout, TimeUnit unit)//嘗試獲取鎖,最多等待 timeout 時長 超時鎖
public void lockInterruptibly()//可中斷鎖,調用線程 interrupt 方法,則鎖方法拋出 InterruptedException  中斷鎖

  具體可查看github鏈接裡面的ReentrantLockTest。

3.3 等待/通知模型

  內置隊列存在一些缺陷,每個內置鎖只能關聯一個條件隊列(_WaitSet),這導致多個線程可能會在同一個條件隊列上等待不同的條件謂詞,如果每次使用notify喚醒條件隊列,可能會喚醒錯誤的線程導致喚醒失敗,但是如果使用notifyAll的話,能喚醒到正確的線程,因為所有的線程都會被喚醒,這也帶來一個問題,就是不應該被喚醒的在被喚醒後發現不是自己等待的條件謂詞轉而又被掛起。這樣的操作會帶來系統的資源浪費,降低系統性能。這個時候推薦使用顯式的Lock和Condition來替代內置鎖和條件隊列,從而控制多個條件謂詞的情況,達到精確的控制線程的喚醒和掛起。具體後面再來分析下JVM的內置鎖、條件隊列模型和顯式的Lock、Condition模型,實際上在AQS裡面也提到了Lock、Condition模型。

3.4 和synchronized比較

  兩者的區別大致如下:

synchronized

ReentrantLock

使用Object本身的wait、notify、notifyAll調度機制

與Condition結合進行線程的調度

顯式的使用在同步方法或者同步代碼塊

顯式的聲明指定起始和結束位置

托管給JVM執行,不會因為異常、或者未釋放而發生死鎖

手動釋放鎖

  Jdk1.6之前,ReentrantLock性能優於synchronized,不過1.6之後,synchronized做了大量的性能調優,而且synchronized相對程式員來說,簡潔熟悉,如果不是synchronized無法實現的功能,如輪詢鎖、超時鎖和中斷鎖等,推薦首先使用synchronized,而針對鎖的高級功能,再使用ReentrantLock。

 

參考資料:

https://github.com/lingjiango/ConcurrentProgramPractice

https://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html

https://www.ibm.com/developerworks/java/library/j-jtp10264/


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

-Advertisement-
Play Games
更多相關文章
  • 開始食用grpc(之一) 轉載請註明出處:https://www.cnblogs.com/funnyzpc/p/9501353.html ``` 記一次和一鍋們壓馬路,路過一咖啡廳(某巴克),隨口就問隨行的鍋門:你能從那咖啡廳看到什麼? 當時的那家某巴克處於鬧市,也正值周末,屋外屋內喝咖啡的人幾近乎 ...
  • # -*- coding: utf-8 -*- """ 主要記錄代碼,相關說明採用註釋形勢,供日常總結、查閱使用,不定時更新。 Created on Mon Aug 20 23:37:26 2018 @author: Dev """ import numpy as np from datetime ...
  • 給定一個贖金信 (ransom) 字元串和一個雜誌(magazine)字元串,判斷第一個字元串ransom能不能由第二個字元串magazines裡面的字元構成。如果可以構成,返回 true ;否則返回 false。 (題目說明:為了不暴露贖金信字跡,要從雜誌上搜索各個需要的字母,組成單詞來表達意思。 ...
  • 我們正在玩一個猜數字游戲。 游戲規則如下: 我從 1 到 n 選擇一個數字。 你需要猜我選擇了哪個數字。 每次你猜錯了,我會告訴你這個數字是大了還是小了。 你調用一個預先定義好的介面 guess(int num),它會返回 3 個可能的結果(-1,1 或 0): -1 : 我的數字比較小 1 : 我 ...
  • # 含多空格字元串的分割 hello = "hello python hello"print(a.split(" ")) # ['hello', 'python', '', 'hello'] print(hello.split()) # ['hello', 'python', 'hello']pri ...
  • eclipse的svn提交不了,報錯。提示 svn: is already locked 解決辦法:右鍵項目 Team Refresh/Cleanup ...
  • import numpy as npimport syssys.setrecursionlimit(1000) #例如這裡設置為一百萬def get1(n): if n<3: return 0 if n<6: return 3 return 6def get2(n): if n<3: return ...
  • springboot的自動配置功能,主要流程如下: 1 啟動的時候載入我們的主配置類,也就是我們的入口類;從而開啟我們的自動配置配置功能,這個是通過@EnableAutoConfiguration註解實現的; 2 @EnableautoConfiguration利用其註解類中的方法:EnableAu ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...