AQS機制


一,Lock接口
  鎖是用來控制多個線程訪問共享資源的方式,一般來說,一個鎖能夠防止多個線程同時訪問共享資源(但是有些鎖可以允許多個線程並發的訪問共享資源,比如讀寫鎖)。在Lock接口出現之前,Java程序是靠synchronized關鍵字實現鎖功能的,而Java SE 5之后,並發包中新增了Lock接口(以及相關實現類)用來實現鎖功能,它提供了與synchronized關鍵字類似的同步功能,只是在使用時需要顯式地獲取和釋放鎖。雖然它缺少了(通過synchronized塊或者方法所提供的)隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖等多種synchronized關鍵字所不具備的同步特性。使用synchronized關鍵字將會隱式地獲取鎖,但是它將鎖的獲取和釋放固化了,也就是先獲取再釋放。當然,這種方式簡化了同步的管理,可是擴展性沒有顯示的鎖獲取和釋放來的好。
1.lock鎖的使用形式
1     Lock lock = new ReentrantLock(); //可以是自己實現的Lock接口的實現類,也可以是jdk提供的同步組件
2  lock.lock();//一般不能放到try語句中 3 try { 4 } finally { 5  lock.unlock(); //一般要求放到finally中,確保即使發生異常也能安全釋放掉鎖 6 } 
  • 在finally塊中釋放鎖,目的是保證在獲取到鎖之后,即使發生異常,鎖依然能被順利釋放,從而避免死鎖情況的發生。
  • 不要將獲取鎖的過程寫在try塊中。假設放到try中,如果在獲取鎖時發生了異常,即鎖沒有被成功獲取到,但finally語句中有釋放鎖的操作,這就會造成死鎖,因為根本沒有獲取到鎖,而底下又要求釋放鎖。如果沒有放到try中,當獲取鎖失敗時,代碼立即會報異常而終止運行,因此就避免了死鎖。
2.Lock接口的方法
  lock接口在java.util.cincurrent.locks包路徑下
 

 3.相比於synchronized,Lock接口所具備的其他特性
  ①嘗試非阻塞的獲取鎖tryLock():當前線程嘗試獲取鎖,如果該時刻鎖沒有被其他線程獲取到,就能成功獲取並持有鎖,接着返回true,如果沒有獲取到則返回false。
  ②能被中斷的獲取鎖lockInterruptibly():獲取鎖的線程能夠響應中斷。當線程在獲取鎖定過程中,如果鎖被其他線程占用,則線程一直處於休眠狀態,直到獲取到鎖或被其他線程中斷才返回。要注意該線程允許其他線程調用Thread.interrupt()方法來中斷等待的線程,當線程被中斷掉,不會在去獲取鎖,會拋出interruptedException異常。
  ③超時的獲取鎖tryLock(long time, TimeUnit unit):在指定的截止時間獲取鎖,如果沒有獲取到鎖返回false。

二,AbstractQueuedSynchronizer介紹
   談到並發,不得不談ReentrantLock;而談到ReentrantLock,不得不談AbstractQueuedSynchronizer(AQS)!
  AbstractQueuedSynchronizer,簡稱AQS(同步器),是用來構建鎖或者其他同步組件的基礎框架,它使用了一個int成員變量表示同步狀態,通過內置的FIFO隊列來完成資源獲取線程的排隊工作,為構建不同的同步組件(重入鎖,讀寫鎖,CountDownLatch等)提供了可擴展的基礎框架,如下圖所示。
    同步器的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態。
  子類推薦被定義為自定義同步組件的靜態內部類,同步器自身沒有實現任何同步接口,它僅僅是定義了若干同步狀態獲取和釋放的方法來供自定義同步組件使用,同步器既可以支持獨占式地獲取同步狀態,也可以支持共享式地獲取同步狀態,這樣就可以方便實現不同類型的同步組件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。
  同步器是實現鎖(也可以是任意同步組件)的關鍵,在鎖的實現中聚合同步器,利用同步器實現鎖的語義。可以這樣理解二者之間的關系:鎖是面向使用者的,它定義了使用者與鎖交互的接口(比如可以允許兩個線程並行訪問),隱藏了實現細節;同步器面向的是鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態管理、線程的排隊、等待與喚醒等底層操作。鎖和同步器很好地隔離了使用者和實現者所需關注的領域。
1.AQS的核心思想
   AQS的核心思想是,如果被請求的共享資源空閑,則將當前請求資源的線程設置為有效的工作線程,並將共享資源設置為鎖定狀態,如果被請求的共享資源被占用,那么就需要一套線程阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH隊列鎖實現的,即將暫時獲取不到鎖的線程加入到隊列中。
  CLH(Craig,Landin,and Hagersten)隊列是一個虛擬的雙向隊列,虛擬的雙向隊列即不存在隊列實例,僅存在節點之間的關聯關系。AQS是將每一條請求共享資源的線程封裝成一個CLH鎖隊列的一個結點(Node),來實現鎖的分配。
  用大白話來說,AQS就是基於CLH隊列,用volatile修飾共享變量state,線程通過CAS去改變狀態符,成功則獲取鎖成功,失敗則進入等待隊列,等待被喚醒。
2.AQS的框架

  在AQS中維護了一個volatile int state(代表共享資源)和一個FIFO存放被阻塞的線程的同步隊列(多線程爭用資源被阻塞時會進入此隊列)。

  其中state可以使用同步器提供的3個方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))來進行操作,因為它們使用CAS操作能夠保證狀態的改變是安全的。

那AQS的該如何使用呢?

  首先,我們需要去繼承AbstractQueuedSynchronizer這個類,然后我們根據我們的需求去重寫相應的方法,比如要實現一個獨占鎖,那就去重寫tryAcquire,tryRelease方法,要實現共享鎖,就去重寫tryAcquireShared,tryReleaseShared;最后,在我們的組件中調用AQS中的模板方法就可以了,而這些模板方法是會調用到我們之前重寫的那些方法的。也就是說,我們只需要很小的工作量就可以實現自己的同步組件,重寫的那些方法,僅僅是一些簡單的對於共享資源state的獲取和釋放操作,至於像是獲取資源失敗,線程需要阻塞之類的操作,自然是AQS幫我們完成了。

  我們來看看AQS定義的這些可重寫的方法:

    protected boolean tryAcquire(int arg) : 獨占式獲取同步狀態,試着獲取,成功返回true,反之為false

    protected boolean tryRelease(int arg) :獨占式釋放同步狀態,等待中的其他線程此時將有機會獲取到同步狀態;

    protected int tryAcquireShared(int arg) :共享式獲取同步狀態,返回值大於等於0,代表獲取成功;反之獲取失敗;

    protected boolean tryReleaseShared(int arg) :共享式釋放同步狀態,成功為true,失敗為false

    protected boolean isHeldExclusively() : 是否在獨占模式下被線程占用。

接下來我們舉一個自定義實現鎖的實例的代碼:

 1 package juc;
 2 import java.util.concurrent.locks.AbstractQueuedSynchronizer;
 3 //Mutex是我們自定的鎖
 4 public class Mutex implements java.io.Serializable {
 5     //靜態內部類,繼承AQS
 6     private static class Sync extends AbstractQueuedSynchronizer {
 7         //是否處於占用狀態
 8         protected boolean isHeldExclusively() {
 9             return getState() == 1;
10         }
11         //當狀態為0的時候獲取鎖,CAS操作成功,則state狀態為1,
12         public boolean tryAcquire(int acquires) {
13             if (compareAndSetState(0, 1)) {
14                 setExclusiveOwnerThread(Thread.currentThread());
15                 return true;
16             }
17             return false;
18         }
19         //釋放鎖,將同步狀態置為0
20         protected boolean tryRelease(int releases) {
21             if (getState() == 0) throw new IllegalMonitorStateException();
22             setExclusiveOwnerThread(null);
23             setState(0);
24             return true;
25         }
26     }
27         //同步對象完成一系列復雜的操作,我們僅需指向它即可
28         private final Sync sync = new Sync();
29         //加鎖操作,代理到acquire(模板方法)上就行,acquire會調用我們重寫的tryAcquire方法
30         public void lock() {
31             sync.acquire(1);
32         }
33         public boolean tryLock() {
34             return sync.tryAcquire(1);
35         }
36         //釋放鎖,代理到release(模板方法)上就行,release會調用我們重寫的tryRelease方法。
37         public void unlock() {
38             sync.release(1);
39         }
40         public boolean isLocked() {
41             return sync.isHeldExclusively();
42         }
43 }

  上面是鎖的實現,其使用的方法和ReentrantLock的使用方法一樣,因為ReentrantLock也是基於AQS實現的。

 

  通過前面介紹AQS的框架和使用方法,我們知道它是基於同步對列和state變量實現的,使用同步隊列來存放被阻塞的線程。接下來就是介紹它是怎樣運用同步隊列的?

3.AQS的同步隊列
  AQS的內部結構主要由同步等待隊列(CLH)構成。同步器依賴內部的FIFO同步隊列(一個虛擬的雙向鏈表)來完成同步狀態的管理,當前線程獲取鎖失敗時,同步器會將當前線程以及等待狀態等信息構造成為一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程,當鎖釋放時,會把下一個等待的節點中的線程喚醒,使其再次嘗試獲取鎖。
  同步隊列中的節點(Node)用來保存獲取同步狀態失敗的線程引用、等待狀態以及前驅和后繼節點,節點的屬性類型與名稱以及描述如下所示。
  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;//當前結點代表的狀態
        Node nextWaiter;

  前面我們提到過,AQS維護一個共享資源state,通過內置的FIFO來完成獲取資源線程的排隊工作。(這個內置的同步隊列稱為"CLH"隊列)。該隊列由一個一個的Node結點組成,每個Node結點維護一個prev引用和next引用,分別指向自己的前驅和后繼結點。AQS維護兩個指針,分別指向隊列頭部head和尾部tail。注意隊列中的第一個元素表示正在使用鎖的線程,而隊列中第二個結點才是第一個真正排隊的結點,同步隊列的基本結構如圖所示。

  

  其實就是個雙端雙向鏈表

為了接下來能夠更好的理解加鎖和解鎖過程的源碼,對該同步隊列的特性進行簡單的講解:

  • 1.同步隊列是個先進先出(FIFO)隊列,獲取鎖失敗的線程將構造結點並加入隊列的尾部,並阻塞自己。如何才能線程安全的實現入隊是后面講解的重點,畢竟我們在講鎖的實現,這部分代碼肯定是不能用鎖的。
  • 2.隊列首結點可以用來表示當前正獲取鎖的線程。
  • 3.當前線程釋放鎖后將嘗試喚醒后續處結點中處於阻塞狀態的線程。

 3.AQS的底層源碼分析

 

 

  之前看的這篇博客感覺寫的不錯,在這里就直接引用下:https://blog.csdn.net/java_lyvee/article/details/98966684

  下面是我根據博客梳理的AQS的tryAcquire()的執行過程圖:

 

 https://www.cnblogs.com/chengxiao/p/7141160.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM