【JAVA重要知識 | 第三篇】深入理解并暴打AQS原理、ReentrantLock鎖

文章目錄

  • 3.深入理解AQS、ReentrantLock
    • 3.1AQS
      • 3.1.1AQS簡介
      • 3.1.2核心結構
        • (1)設計模型
        • (2)組成部分
        • (3)State關鍵字
      • 3.1.3實現的兩類隊列
        • (1)同步隊列
          • ①CLH
          • ②Node
          • ③主要行為
        • img條件隊列
      • 3.1.4總結
    • 3.2ReentrantLock
      • 3.2.1Synchronized和ReentrantLock
        • (1)性能上的比較
        • (2)獲取公平鎖
        • (3)綜述
      • 3.2.2可重入功能的實現原理
      • 3.2.3非公平鎖的實現原理
        • (1)加鎖
        • (2)釋放鎖
      • 3.2.4公平鎖的實現原理
      • 3.2.4tryLock原理
      • 3.2.5可中斷的獲取鎖
      • 3.2.6可超時的獲取鎖
    • 3.2.7小結

3.深入理解AQS、ReentrantLock

在這里插入圖片描述

3.1AQS

3.1.1AQS簡介

AQS即隊列同步器AbstractQueuedSynchronizer(后面簡稱AQS)是實現鎖和有關同步器的一個基礎框架。

在JDK5中,Doug Lea在并發包中加入了大量的同步工具,例如==重入鎖(ReentrantLock)、讀寫鎖(ReentrantReadWriteLock)、信號量(Semaphore)、CountDownLatch(倒計時鎖)==等,都是基于AQS的。

其內部通過一個被標識為volatile的名為state的變量來控制多個線程之間的同步狀態。多個線程之間可以通過AQS來獨占式或共享式的搶占資源。

基于AQS,可以很方便的實現Java中不具備的功能。

例如,在鎖這個問題上,Java中提供的是synchronized關鍵字,用這個關鍵字可以很方便的實現多個線程之間的同步。但這個關鍵字也有很多缺陷,比如:

  • 不支持超時的獲取鎖:一個線程一旦沒有從synchronized上獲取鎖,就會卡在這里,沒有機會逃脫。所以通常由synchronized造成的死鎖是無解的。
  • 不可響應中斷
  • 不能嘗試獲取鎖。如果嘗試獲取時沒獲取到,立刻返回,synchronized不具備這一特性。

而ReentrantLock基于AQS將上述幾點都做到了。

3.1.2核心結構

從AbstractQueuedSynchronizer的名字可以看出,AQS中一定是基于隊列實現的(Queue)

在AQS內部,是通過鏈表實現的隊列

鏈表的每個元素是其內部類Node的一個實現。然后AQS通過實例變量head指向隊列的頭,通過實例變量tail指向隊列的尾。

img

其源碼定義如下:

/*** Head of the wait queue, lazily initialized.  Except for* initialization, it is modified only via method setHead.  Note:* If head exists, its waitStatus is guaranteed not to be* CANCELLED.*/
private transient volatile Node head;/*** Tail of the wait queue, lazily initialized.  Modified only via* method enq to add new wait node.*/
private transient volatile Node tail;/*** The synchronization state.*/
private volatile int state;static final class Node {/** 標識為共享式 */static final Node SHARED = new Node();/** 標識為獨占式 */static final Node EXCLUSIVE = null;/** 同步隊列中等待的線程等待超時或被中斷,需要從等待隊列中取消等待,進入該狀態的節點狀態將不再變化 */static final int CANCELLED =  1;/** 當前節點的后繼節點處于等待狀態,且當前節點釋放了同步狀態,需要通過unpark喚醒后繼節點,讓其繼續運行 */static final int SIGNAL    = -1;/** 當前節點等待在某一Condition上,當其他線程調用這個Conditino的signal方法后,該節點將從等待隊列恢復到同步隊列中,使其有機會獲取同步狀態 */static final int CONDITION = -2;/** 表示下一次共享式同步狀態獲取狀態將無條件的傳播下去 */static final int PROPAGATE = -3;/* 當前節點的等待狀態,取值為上述幾個常量之一,另外,值為0表示初始狀態 */volatile int waitStatus;/* 前驅節點 */volatile Node prev;/* 后繼節點 */volatile Node next;/* 等待獲取同步狀態的線程 */volatile Thread thread;/* 等待隊列中的后繼節點 */Node nextWaiter;// ...
}
(1)設計模型

img

(2)組成部分

AQS 主要由三部分組成

  1. state 同步狀態
  2. Node 組成的 CLH 隊列
  3. ConditionObject 條件變量(包含 Node 組成的條件單向隊列)。

state 用 volatile 來修飾,保證了我們操作的可見性,所以任何線程通過 getState() 獲得狀態都是可以得到最新值,但是 setState() 無法保證原子性,因此 AQS 給我們提供了 compareAndSetState 方法利用底層 UnSafe 的 CAS 功能來實現原子性。

(3)State關鍵字

對于 AQS 來說,線程同步的關鍵是對 state 的操作,可以說獲取、釋放資源是否成功都是由 state 決定的,比如 state>0 代表可獲取資源,否則無法獲取,所以 state 的具體語義由實現者去定義,現有的 ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch 定義的 state 語義都不一樣。

  • ReentrantLock 的 state 用來表示是否有鎖資源,變量記錄了鎖的重入次數
  • ReentrantReadWriteLockstate 高 16 位代表讀鎖狀態,低 16 位代表寫鎖狀態
  • Semaphore 的 state 用來表示可用信號的個數
  • CountDownLatchstate 用來表示計數器的值

3.1.3實現的兩類隊列

  1. 同步隊列:服務于線程阻塞等待獲取資源
  2. 條件隊列:服務于線程因某個條件不滿足而進入等待狀態。 條件隊列中的線程實際上已經獲取到了資源,但是沒有能夠繼續執行下去的條件,所以被打入條件隊列并釋放持有的資源,以讓渡其它線程執行,如果未來某個時刻條件得以滿足,則該線程會被從條件隊列轉移到同步隊列,繼續參與競爭資源,以繼續向下執行。
(1)同步隊列
①CLH

同步隊列是基于鏈表實現的雙向隊列,也是 CLH 鎖的變種。CLH 鎖是 AQS 隊列同步器實現的基礎。

以下圖為CLH的構成

img

  1. CLH 鎖是有由 Craig, Landin, and Hagersten 這三個人發明的鎖,取了三個人名字的首字母,所以叫 CLH Lock
  2. CLH 鎖是一個自旋鎖。能確保無饑餓性。提供先來先服務的公平性。
  3. CLH 隊列鎖也是一種基于鏈表的可擴展、高性能、公平的自旋鎖,申請線程僅僅在本地變量上自旋,它不斷輪詢前驅的狀態,假設發現前驅釋放了鎖就結束自旋
②Node

AQS 以內部類 Node 的形式定義了同步隊列結點。這就是前文看到的第一個內部類。

static final class Node {/** 模式定義 */static final Node SHARED = new Node();static final Node EXCLUSIVE = null;/** 線程狀態 */static final int CANCELLED = 1;static final int SIGNAL = -1;static final int CONDITION = -2;static final int PROPAGATE = -3;/** 線程等待狀態 */volatile int waitStatus;/** 前驅結點 */volatile Node prev;/** 后置結點 */volatile Node next;/** 持有的線程對象 */volatile Thread thread;/** 對于獨占模式而言,指向下一個處于 CONDITION 等待狀態的結點;對于共享模式而言,則為 SHARED 結點 */Node nextWaiter;// ... 省略方法定義
}

Node 在 CLH 的基礎上進行了變種。

CLH 是單向隊列,其主要特點是自旋檢查前驅節點的 locked 狀態

而 AQS 同步隊列是 雙向隊列,每個節點也有狀態 waitStatus,而其并不是一直對前驅節點的狀態自旋判斷,而是自旋一段時間后阻塞讓出 cpu 時間片(上下文切換),等待前驅節點主動喚醒后繼節點

waitStatus 有如下 5 中狀態:

  • CANCELLED = 1 表示當前結點已取消調度。當超時或被中斷(響應中斷的情況下),會觸發變更為此狀態,進入該狀態后的結點將不會再變化。
  • SIGNAL = -1 表示后繼結點在等待當前結點喚醒。后繼結點入隊時,會將前繼結點的狀態更新為 SIGNAL。
  • CONDITION = -2 表示結點等待在 Condition 上,當其他線程調用了 Condition 的 signal() 方法后,CONDITION 狀態的結點將從等待隊列轉移到同步隊列中,等待獲取同步鎖。
  • PROPAGATE = -3 共享模式下,前繼結點不僅會喚醒其后繼結點,同時也可能會喚醒后繼的后繼結點。
  • INITIAL = 0 新結點入隊時的默認狀態

從上面的代碼中可以看出,位于 CLH 鏈表中的線程以 2 種模式在等待資源,即 SHARED 和 EXCLUSIVE,其中 SHARED 表示共享模式,而 EXCLUSIVE 表示獨占模式。

共享模式與獨占模式的主要區別在于,同一時刻獨占模式只能有一個線程獲取到資源,而共享模式在同一時刻可以有多個線程獲取到資源。典型的場景就是讀寫鎖,讀操作可以有多個線程同時獲取到讀鎖資源,而寫操作同一時刻只能有一個線程獲取到寫鎖資源,其它線程在嘗試獲取資源時都會被阻塞。

③主要行為

AQS 類成員變量 head 和 tail 字段分別指向同步隊列的頭結點和尾結點:

	 /*** Head of the wait queue, lazily initialized.  Except for* initialization, it is modified only via method setHead.  Note:* If head exists, its waitStatus is guaranteed not to be* CANCELLED.*/private transient volatile Node head;/*** Tail of the wait queue, lazily initialized.  Modified only via* method enq to add new wait node.*/private transient volatile Node tail;

其中 head 表示同步隊列的頭結點,而 tail 則表示同步隊列的尾結點,具體組織形式如下圖:

img

當調用 AQS 的 acquire 方法獲取資源時,如果資源不足則當前線程會被封裝成 Node 結點添加到同步隊列的末端(入隊),頭結點 head 用于記錄當前正在持有資源的線程結點,而 head 的后繼結點就是下一個將要被調度的線程結點,當 release 方法被調用時,該結點上的線程將被喚醒(出隊),繼續獲取資源。

同步隊列的主要行為是 :入隊、出隊

  • 入隊

獲取資源失敗的線程需要封裝成 Node 節點,接著尾部入隊,在 AQS 中提供 addWaiter 函數完成 Node 節點的創建與入隊。添加節點的時候,如 CLH 隊列已經存在,通過 CAS 快速將當前節點添加到隊列尾部,如果添加失敗或隊列不存在,則初始化同步隊列。

/*** Creates and enqueues node for current thread and given mode.** @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared* @return the new node*/
private Node addWaiter(Node mode) {Node node = new Node(mode);for (;;) {Node oldTail = tail;if (oldTail != null) {node.setPrevRelaxed(oldTail);if (compareAndSetTail(oldTail, node)) {oldTail.next = node;return node;}} else {initializeSyncQueue();}}
}

總結:線程獲取鎖失敗,入隊列,將新節點加到 tail 后面然后對 tail 進行 CAS 操作,將 tail 指針后移到新節點上

  • 出隊

CLH 隊列中的節點都是獲取資源失敗的線程節點,當持有資源的線程釋放資源時,會將 head.next 指向的線程節點喚醒(CLH 隊列的第二個節點),如果喚醒的線程節點獲取資源成功,線程節點清空信息設置為頭部節點(新哨兵節點),原頭部節點出隊(原哨兵節點)

 protected final boolean tryRelease(int releases) {int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) { // 如果 state=0 了,就是可以釋放鎖了free = true; setExclusiveOwnerThread(null); // 將拿鎖線程置為 null}setState(c); // 重置同步器的 statereturn free; // 返回是否成功釋放}private void unparkSuccessor(Node node) {// node 節點是當前釋放鎖的節點,也是同步隊列的頭節點int ws = node.waitStatus;// 如果節點已經被取消了,把節點的狀態置為初始化if (ws < 0)compareAndSetWaitStatus(node, ws, 0);// 拿出隊二 sNode s = node.next;// s 為空,表示 node 的后一個節點為空// s.waitStatus 大于 0,代表 s 節點已經被取消了// 遇到以上這兩種情況,就從隊尾開始,向前遍歷,找到第一個 waitStatus 字段不是被取消的if (s == null || s.waitStatus > 0) {s = null;// 結束條件是前置節點就是 head 了for (Node t = tail; t != null && t != node; t = t.prev)// t.waitStatus <= 0 說明 t 當前沒有被取消,肯定還在等待被喚醒if (t.waitStatus <= 0)s = t;}// 喚醒以上代碼找到的線程if (s != null)LockSupport.unpark(s.thread);
}

總結:出隊列,鎖釋放喚醒 head 的后繼節點,head 的后繼節點從阻塞中醒來,開始搶鎖,獲取鎖成功此時 head 指針向后移一個位置,原先 head 的后繼節點成為新的 head

img(2)條件隊列

一個 AQS 可以對應多個條件變量

img

ConditionObject 內部維護著一個單向條件隊列,不同于 CLH 隊列,條件隊列只入隊執行 await 的線程節點,并且加入條件隊列的節點,不能在 CLH 隊列, 條件隊列出隊的節點,會入隊到 CLH 隊列

當某個線程執行了 ConditionObject 的 await 函數,阻塞當前線程,線程會被封裝成 Node 節點添加到條件隊列的末端,其他線程執行 ConditionObject 的 signal 函數,會將條件隊列頭部線程節點轉移到 CLH 隊列參與競爭資源,具體流程如下圖:

img

一個 Condition 對象就有一個單項的等待任務隊列。在一個多線程任務中我們可以 new 出多個等待任務隊列。比如我們 new 出來兩個等待隊列。

 private Lock lock = new ReentrantLock();private Condition FirstCond = lock.newCondition();private Condition SecondCond = lock.newCondition();

所以真正的 AQS 任務中一般是一個任務隊列 N 個等待隊列的,因此我們盡量調用 signal 而少用 signalAll,因為在指定的實例化等待隊列中只有一個可以拿到鎖的。

3.1.4總結

  1. 狀態管理:AQS 內部維護了一個狀態變量state,通過該狀態變量來表示共享資源的狀態,可以是獨占模式也可以是共享模式
  2. CAS 操作:AQS 使用 CAS(Compare And Swap)操作來實現對狀態變量的原子性修改,確保線程安全性。
  3. 線程阻塞和喚醒:當一個線程嘗試獲取鎖或訪問資源時,如果資源已被其他線程占用,則會將該線程阻塞并加入同步等待隊列,直到資源可用時再喚醒線程。
  4. 雙向鏈表:AQS 使用雙向鏈表來管理等待線程,保持線程之間的先后順序,即按照先進先出的原則進行訪問。
  5. 模板方法設計模式:AQS 提供了模板方法,允許子類通過實現特定的方法來控制鎖的獲取和釋放過程,從而實現不同類型的同步器。

3.2ReentrantLock

ReentrantLock,重入鎖,是JDK5中添加在并發包下的一個高性能的工具。

顧名思義,ReentrantLock支持同一個線程在未釋放鎖的情況下重復獲取鎖

3.2.1Synchronized和ReentrantLock

(1)性能上的比較

首先,ReentrantLock的性能要優于synchronized。下面通過兩段代碼比價一下。 首先是synchronized:

PS:當存在大量線程競爭鎖時,多數情況下ReentrantLock的性能優于synchronized。

因為在JDK6中對synchronized做了優化

鎖競爭不激烈的時候,多數情況下鎖會停留在偏向鎖和輕量級鎖階段,這兩個階段性能是很好的。

當存在大量競爭時,可能會膨脹為重量級鎖,性能下降,此時的ReentrantLock應該是優于synchronized的。

(2)獲取公平鎖

公平性是啥概念呢?如果是公平的獲取鎖,就是說多個線程之間獲取鎖的時候要排隊,依次獲取鎖;如果是不公平的獲取鎖,就是說多個線程獲取鎖的時候一哄而上,誰搶到是誰的。

由于synchronized是基于monitor機制實現的,它只支持非公平鎖

但ReentrantLock同時支持公平鎖和非公平鎖。

(3)綜述

ReentrantLock還有一些其他synchronized不具備的特性,這里來總結一下。

img

3.2.2可重入功能的實現原理

ReentrantLock的實現基于隊列同步器(AbstractQueuedSynchronizer)后面簡稱AQS

ReentrantLock的可重入功能基于AQS的同步狀態:state

核心原理:當某一線程獲取鎖后,將state值+1,并記錄下當前持有鎖的線程

再有線程來獲取鎖時,判斷這個線程與持有鎖的線程是否是同一個線程,如果是,將state值再+1,如果不是,阻塞線程。 當線程釋放鎖時,將state值-1

當state值減為0時,表示當前線程徹底釋放了鎖,然后將記錄當前持有鎖的線程的那個字段設置為null,并喚醒其他線程,使其重新競爭鎖。

// acquires的值是1
final boolean nonfairTryAcquire(int acquires) {// 獲取當前線程final Thread current = Thread.currentThread();// 獲取state的值int c = getState();// 如果state的值等于0,表示當前沒有線程持有鎖// 嘗試將state的值改為1,如果修改成功,則成功獲取鎖,并設置當前線程為持有鎖的線程,返回trueif (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}// state的值不等于0,表示已經有其他線程持有鎖// 判斷當前線程是否等于持有鎖的線程,如果等于,將state的值+1,并設置到state上,獲取鎖成功,返回true// 如果不是當前線程,獲取鎖失敗,返回falseelse if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}

3.2.3非公平鎖的實現原理

ReentrantLock有兩個構造函數:

// 無參構造,默認使用非公平鎖(NonfairSync)
public ReentrantLock() {sync = new NonfairSync();
}// 通過fair參數指定使用公平鎖(FairSync)還是非公平鎖(NonfairSync)
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();
}

sync是ReentrantLock的成員變量,是其內部類Sync的實例。NonfairSync和FairSync都是Sync類的子類。可以參考如下類關系圖:

img

Sync繼承了AQS,所以他具備了AQS的功能。同樣的,NonfairSync和FairSync都是AQS的子類。

當我們通過無參構造函數獲取ReentrantLock實例后,默認用的就是非公平鎖。

(1)加鎖

下面將通過如下場景描述非公平鎖的實現原理:假設一個線程(t1)獲取到了鎖,其他很多沒獲取到鎖的線程(others_t)加入到了AQS的同步隊列中等待,當這個線程執行完,釋放鎖后,其他線程重新非公平的競爭鎖。

注意:新來的線程執行lock方法時,都要嘗試下能不能直接獲取鎖,若獲取鎖成功,則記錄當前線程,否則調用AQS的acqurire方法,進入到同步隊列中等待

final void lock() {// 線程t1成功的將state的值從0改為1,表示獲取鎖成功// 并記錄當前持有鎖的線程if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());else// others_t線程們沒有獲取到鎖acquire(1);
}

如果獲取鎖失敗,會調用AQS的acquire方法

public final void acquire(int arg) {// tryAcquire是個模板方法,在NonfairSync中實現,如果在tryAcquire方法中依然獲取鎖失敗,會將當前線程加入同步隊列中等待(addWaiter)if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}

tryAcquire的實現如下,其實是調用了上面的nonfairTryAcquire方法

protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);
}
(2)釋放鎖

OK,此時t1獲取到了鎖,others_t線程們都跑到同步隊列里等著了

某一時刻,t1自己的任務執行完成,調用了釋放鎖的方法(unlock)。

public void unlock() {// 調用AQS的release方法釋放資源sync.release(1);
}
public final boolean release(int arg) {// tryRelease也是模板方法,在Sync中實現if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)// 成功釋放鎖后,喚醒同步隊列中的下一個節點,使之可以重新競爭鎖// 注意此時不會喚醒隊列第一個節點之后的節點,這些節點此時還是無法競爭鎖unparkSuccessor(h);return true;}return false;
}
protected final boolean tryRelease(int releases) {// 將state的值-1,如果-1之后等于0,釋放鎖成功int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;
}

這時鎖被釋放了,喚醒的線程(一個)和新來的線程重新競爭鎖不包含同步隊列后面的那些線程)。

回到lock方法中,由于此時所有線程都能通過CAS來獲取鎖,并不能保證被喚醒的那個線程能競爭過新來的線程,所以是非公平的。這就是非公平鎖的實現。

這個過程大概可以描述為下圖這樣子:

img

3.2.4公平鎖的實現原理

公平鎖與非公平鎖的釋放鎖的邏輯是一樣的,都是調用上述的unlock方法,最大區別在于獲取鎖的時候直接調用的AQS的acquire方法,沒有先嘗試獲取鎖

static final class FairSync extends Sync {private static final long serialVersionUID = -3000897897090466540L;// 獲取鎖,與非公平鎖的不同的地方在于,這里直接調用的AQS的acquire方法,沒有先嘗試獲取鎖// acquire又調用了下面的tryAcquire方法,核心在于這個方法final void lock() {acquire(1);}/*** 這個方法和nonfairTryAcquire方法只有一點不同,在標注為#1的地方* 多了一個判斷hasQueuedPredecessors,這個方法是判斷當前AQS的同步隊列中是否還有等待的線程* 如果有,返回true,否則返回false。* 由此可知,當隊列中沒有等待的線程時,當前線程才能嘗試通過CAS的方式獲取鎖。* 否則就讓這個線程去隊列后面排隊。*/protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// #1if (!hasQueuedPredecessors() &&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;}
}

通過注釋可知,在公平鎖的機制下,任何線程想要獲取鎖,都要排隊,不可能出現插隊的情況。這就是公平鎖的實現原理。

這個過程大概可以描述為下圖這樣子:

img

3.2.4tryLock原理

tryLock做的事情很簡單:讓當前線程嘗試獲取一次鎖,成功的話返回true,否則false

其實就是調用了nonfairTryAcquire方法來獲取鎖。

public boolean tryLock() {return sync.nonfairTryAcquire(1);
}

至于獲取失敗的話,他也不會將自己添加到同步隊列中等待,直接返回false,讓業務調用代碼自己處理。

3.2.5可中斷的獲取鎖

中斷,也就是通過Thread的interrupt方法將某個線程中斷,中斷一個阻塞狀態的線程,會拋出一個InterruptedException異常。

如果獲取鎖是可中斷的,當一個線程長時間獲取不到鎖時,我們可以主動將其中斷,可避免死鎖的產生。

其實現方式如下:

public void lockInterruptibly() throws InterruptedException {sync.acquireInterruptibly(1);
}

會調用AQS的acquireInterruptibly方法:

public final void acquireInterruptibly(int arg)throws InterruptedException {// 判斷當前線程是否已經中斷,如果已中斷,拋出InterruptedException異常if (Thread.interrupted())throw new InterruptedException();if (!tryAcquire(arg))doAcquireInterruptibly(arg);
}

此時會優先通過tryAcquire嘗試獲取鎖,如果獲取失敗,會將自己加入到隊列中等待,并可隨時響應中斷。

private void doAcquireInterruptibly(int arg)throws InterruptedException {// 將自己添加到隊列中等待final Node node = addWaiter(Node.EXCLUSIVE);boolean failed = true;try {// 自旋的獲取鎖for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return;}// 獲取鎖失敗,在parkAndCheckInterrupt方法中,通過LockSupport.park()阻塞當前線程,// 并調用Thread.interrupted()判斷當前線程是否已經被中斷// 如果被中斷,直接拋出InterruptedException異常,退出鎖的競爭隊列if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())// #1throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}
}

PS:不可中斷的方式下,代碼#1位置不會拋出InterruptedException異常,只是簡單的記錄一下當前線程被中斷了。

3.2.6可超時的獲取鎖

通過如下方法實現,timeout是超時時間,unit代表時間的單位(毫秒、秒…)

public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException {return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

可以發現,這也是一個可以響應中斷的方法。然后調用AQS的tryAcquireNanos方法:

public final boolean tryAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();return tryAcquire(arg) ||doAcquireNanos(arg, nanosTimeout);
}

doAcquireNanos方法與中斷里面的方法大同小異,下面在注釋中說明一下不同的地方:

private boolean doAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {if (nanosTimeout <= 0L)return false;// 計算超時截止時間final long deadline = System.nanoTime() + nanosTimeout;final Node node = addWaiter(Node.EXCLUSIVE);boolean failed = true;try {for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return true;}// 計算到截止時間的剩余時間nanosTimeout = deadline - System.nanoTime();if (nanosTimeout <= 0L) // 超時了,獲取失敗return false;// 超時時間大于1000納秒時,才阻塞// 因為如果小于1000納秒,基本可以認為超時了(系統調用的時間可能都比這個長)if (shouldParkAfterFailedAcquire(p, node) &&nanosTimeout > spinForTimeoutThreshold)LockSupport.parkNanos(this, nanosTimeout);// 響應中斷if (Thread.interrupted())throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}
}

3.2.7小結

本文首先對比了元老級的鎖synchronized與ReentrantLock的不同,ReentrantLock具有一下優勢: 同時支持公平鎖與非公平鎖 支持:嘗試非阻塞的一次性獲取鎖 支持超時獲取鎖 支持可中斷的獲取鎖 * 支持更多的等待條件(Condition)

然后介紹了幾個主要特性的實現原理,這些都是基于AQS的。

  1. 首先,ReentrantLock 采用了獨占模式,即同一時刻只允許一個線程持有鎖。這保證了被鎖保護的臨界區只能被一個線程訪問,從而避免了多個線程同時修改共享資源導致的數據競爭和不一致性。
  2. ReentrantLock的核心,是通過修改AQS中state的值來同步鎖的狀態。 通過這個方式,實現了可重入
  3. ReentrantLock具備公平鎖和非公平鎖,默認使用非公平鎖。其實現原理主要依賴于AQS中的同步隊列。

最后,可中斷的機制是內部通過Thread.interrupted()判斷當前線程是否已被中斷,如果被中斷就拋出InterruptedException異常來實現的。

在這里插入圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/718559.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/718559.shtml
英文地址,請注明出處:http://en.pswp.cn/news/718559.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

中霖教育:注冊安全工程師考是科目有哪些?

注冊安全工程師的類型是職業資格證書&#xff0c;需要滿足報名條件才能參加考試&#xff0c;考試通過就能發放證書。報名時間一般在八月份&#xff0c;考試時間在十月底左右。 考試科目&#xff1a; 《安全生產法律法規》 《安全生產管理》 《安全生產技術基礎》 《安全生…

golang實現openssl自簽名雙向認證

第一步&#xff1a;生成CA、服務端、客戶端證書 1. 生成CA根證書 生成CA證書私鑰 openssl genrsa -out ca.key 4096創建ca.conf 文件 [ req ] default_bits 4096 distinguished_name req_distinguished_name[ req_distinguished_name ] countryName …

Node.js基礎---Express路由

1. 路由的概念 1. 什么是路由 廣義上來講&#xff0c;路由就是映射關系 2. Express 中的路由 在 Express 中&#xff0c;路由指的是客戶端的請求與服務器處理函數之間的映射關系 Express 中的路由分三部分&#xff1a;請求的類型、請求的URL地址&#xff0c;處理函數。如下&am…

怎么使用curl2py自動構造爬蟲代碼并進行網絡爬蟲

目錄 一、了解curl2py 二、安裝curl2py 三、使用curl2py生成爬蟲代碼 四、實際案例&#xff1a;爬取網頁數據 五、總結與建議 在當今數據驅動的時代&#xff0c;網絡爬蟲成為了獲取數據的重要工具。對于初學者來說&#xff0c;手動編寫爬蟲代碼可能是一項挑戰。幸運的是&a…

PyTorch-神經網絡

神經網絡&#xff0c;這也是深度學習的基石&#xff0c;所謂的深度學習&#xff0c;也可以理解為很深層的神經網絡。說起這里&#xff0c;有一個小段子&#xff0c;神經網絡曾經被打入了冷宮&#xff0c;因為SVM派的崛起&#xff0c;SVM不了解的同學可以去google一下&#xff0…

JavaScript 基礎學習筆記(五):函數、作用域、匿名函數

目錄 一、函數 1.1 聲明和調用 1.2 形參和實參 1.3 返回值 二、作用域 2.1 全局作用域 2.2 局部作用域 三、匿名函數 3.1 函數表達式 3.2 立即執行函數 一、函數 理解函數的封裝特性&#xff0c;掌握函數的語法規則 1.1 聲明和調用 函數可以把具有相同或相似邏輯的代…

NLP_文本張量表示方法(代碼示例)

目標 了解什么是文本張量表示及其作用.文本張量表示的幾種方法及其實現. 1 文本張量表示 將一段文本使用張量進行表示&#xff0c;其中一般將詞匯為表示成向量&#xff0c;稱作詞向量&#xff0c;再由各個詞向量按順序組成矩陣形成文本表示. ["人生", "該&q…

無極低碼:五分鐘快速上手,開啟編程新時代

無極低碼平臺憑借其革命性的設計理念和強大的功能特性&#xff0c;正在徹底改變軟件開發的傳統格局。該平臺專為開發者、初創企業和各類研發團隊量身打造&#xff0c;旨在提供一種快速而高效的解決方案&#xff0c;以應對日益增長的業務需求和技術挑戰。 1.無極低碼的核心價值在…

2024《》

vue-cli到哪做了那些事 vue-cli是vue.js的腳手架&#xff0c;用于自動生成vue.jswebpack的項目模板&#xff0c;快速搭建Vue.js項目。 vue cli內置了webpack的一些功能&#xff0c;這些是用webpack打包時需要我們自己配置的&#xff0c;例如&#xff1a; 1.ES6代碼轉換成ES5代…

Linux 實現打印彩色進度條

文章目錄 預備知識一、理解回車換行二、認識行緩沖1、代碼一、二&#xff08;回車換行理解&#xff09;2、代碼三、四&#xff08;sleep函數和ffush函數理解&#xff09; 三、簡單倒計時1. 倒計時代碼2、效果展示 四、進度條1、效果展示2、進度條代碼makefileProcessBar.hProce…

tomcat 反向代理 自建博客 修改狀態頁 等

一 自建博客 隨后&#xff0c;拷貝到webapps下面 并且做軟連接 隨后重定向 并且下載 cat >/etc/yum.repos.d/mysql.repo <<EOF [mysql57-community] nameMySQL 5.7 Community Server baseurlhttp://repo.mysql.com/yum/mysql-5.7-community/el/7/x86_64/ enabled1 g…

團體程序設計天梯賽 L2-006 樹的遍歷

L2-006 樹的遍歷 分數 25 給定一棵二叉樹的后序遍歷和中序遍歷&#xff0c;請你輸出其層序遍歷的序列。這里假設鍵值都是互不相等的正整數。 輸入格式&#xff1a; 輸入第一行給出一個正整數N&#xff08;≤30&#xff09;&#xff0c;是二叉樹中結點的個數。第二行給出其后…

【Linux】Linux系統磁盤分區和掛載相關命令介紹

Linux系統磁盤分區和掛載相關命令介紹 文章目錄 Linux系統磁盤分區和掛載相關命令介紹磁盤分區1、使用fdisk創建分區2、使用parted創建分區 格式化分區分區掛載自動掛載其他常見&#xff08;用&#xff09;的磁盤相關命令 在Linux系統中&#xff0c;磁盤分區和磁盤掛載是管理存…

第十四屆藍橋杯大賽B組 JAVA 蝸牛 (遞歸剪枝)

題目描述&#xff1a; 這天&#xff0c;一只蝸牛來到了二維坐標系的原點。 在 x 軸上長有 n 根竹竿。它們平行于 y 軸&#xff0c;底部縱坐標為 0&#xff0c;橫坐標分別為 x1, x2, …, xn。竹竿的高度均為無限高&#xff0c;寬度可忽略。蝸牛想要從原點走到第 n 個竹竿的底部也…

全域電商數據集成管理與采集|API接口的采集與管理

如今&#xff0c;全渠道零售已是大勢所趨。企業電商經營的一大現狀就是數據分散各處&#xff0c;比如有來自電商平臺私域數據、品牌一方數據、公開的第三方行業數據與電商平臺C端頁面數據等等。如何集成全域數據日益成為企業數字化基建的難題。 當前電商數據集成的主流方案為人…

【基于Matlab GUI的語音降噪系統設計】

客戶不要了&#xff0c;掛網上吧&#xff0c;有需要自行下載~ 賺點辛苦費 ** 功能實現: ** 1、導入音頻文件/錄入音頻&#xff0c;能實現播放功能。 2、對導入/錄入的音頻信號進行時域和頻域分析&#xff0c;并制圖。 3、可在導入/錄入的音頻信號上加入噪聲&#xff0c;并能夠播…

Apache JMeter 5.6.3 安裝

源碼下載 curl -O https://dlcdn.apache.org//jmeter/source/apache-jmeter-5.6.3_src.zipJMeter 下載 curl -O https://dlcdn.apache.org//jmeter/binaries/apache-jmeter-5.6.3.zipjmeter.properties 里 設置中文 windows系統上解壓&#xff0c;雙擊jmeter.bat 啟動 執行參…

【人工智能】DeepLearning學習路線及簡要說明

目錄 神經網絡 1.1 前饋神經網絡(FNN) 結構和工作原理 訓練過程 應用

架構設計方法(4A架構)-應用架構

1、應用架構&#xff08;AA&#xff09;&#xff1a;業務價值與產品之間的橋梁&#xff0c;是企業架構的一個子集 2、應用架構包含“應用系統模塊、應用服務、應用系統集成”3個關鍵要素 3、收集AS-IS應用架構&#xff0c;描繪現狀&#xff0c;并識別改進機會點 4、描述對新系統…

uniapp 安卓YYEVAPlayer MP4禮物播放器原生插件

插件介紹 安卓YYEVAPlayer MP4禮物播放器原生插件&#xff0c;是一個輕量的動畫渲染庫&#xff0c;使用Native Opengles 渲染視頻&#xff0c;為你提供高性能、低開銷的動畫體驗 對比傳統的序列幀的動畫播放方式&#xff0c;具有更高的壓縮率&#xff0c;硬解碼效率更高的優點…