Java並發原理層面:ReentrantLock中lock()、unlock()全解析


一、前言

Java線程同步兩種方式,synchronized關鍵字和Lock鎖機制,其中,AQS隊列就是Lock鎖實現公平加鎖的底層支持。

二、AQS源碼對於lock.lock()的實現

2.1 AQS類 + 內部Node類

2.1.1 AQS類結構示意圖

首先我們要看看AQS的結構的類圖

在這里插入圖片描述
從AQS類的類結構示意圖可以知道,

  1. AbstractQueuedSynchronizer的父類是AbstractOwnableSynchronizer;
  2. AbstractQueuedSynchronizer的子類是Sync,然后又通過繼承Sync得到了FairSync公平鎖和UnfaiSync非公平鎖,所以,AQS是Lock鎖機制實現公平鎖的底層支持。

2.1.2 內部Node類

通過上面的結構圖,我們可以知道該類維護了一個Node內部類,於是我們查看Node的源碼如下,主要是用來實現上面的我們提到的隊列。

static final class Node {

        //指示節點正在共享模式下等待的標記
        static final Node SHARED = new Node();

        //指示節點正在以獨占模式等待的標記
        static final Node EXCLUSIVE = null;

        //waitStatus值,指示線程已取消  cancel 
        // 這個節點已經被取消 canceled 這樣可讀性強
        static final int CANCELLED =  1;

        //waitStatus值,指示后續線程需要釋放  signal  
        // 這個節點的后繼被阻塞,因此當前節點在取消必須釋放它的后繼
        static final int SIGNAL    = -1;

        //waitStatus值,指示線程正在等待條件  condition
        // 這個節點在條件隊列里面
        static final int CONDITION = -2;

        //waitStatus值,表示下一個被默認的應該無條件傳播的等待狀態值 propagate
        static final int PROPAGATE = -3;

    	/*
    	 * SIGNAL:這個節點的后繼被(或即將)阻塞(通過park),因此當前節點在釋放或取消時必須釋放它的后繼。為了避免競爭,acquire方法必須首先表明它們需要一個信號,然后重試原子獲取,當失敗時,阻塞。
         *
         * CANCELLED:由於超時或中斷,該節點被取消。節點不會離開這個狀態。特別是,取消節點的線程不會再次阻塞。
         *
         * CONDITION:此節點當前處於條件隊列中。在傳輸之前,它不會被用作同步隊列節點,此時狀態將被設置為0。
         *
         * PROPAGATE:釋放的共享應該傳播到其他節點。在doReleaseShared中設置這個(僅針對頭節點),以確保傳播繼續,即使其他操作已經干預。
         *
         * 0:以上都不是
    	*/
        volatile int waitStatus;   // 默認值0,什么都不是

        //上一個節點
        volatile Node prev;

        //下一個節點
        volatile Node next;

        //節點中的值
        volatile Thread thread;

        //下一個等待節點
        Node nextWaiter;

        //判斷是否是共享的節點
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        //返回當前的節點前置節點
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        //用於建立初始標頭或SHARED標記
        Node() {    
        }

        //addWaiter時候調用 
        Node(Thread thread, Node mode) {     
            this.nextWaiter = mode;
            this.thread = thread;
        }

        //Condition時候調用
        Node(Thread thread, int waitStatus) { 
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }
復制代碼

關於waitStatus:

  1. SIGNAL=-1:這個節點的后繼被(或即將)阻塞(通過park),因此當前節點在釋放或取消時必須釋放它的后繼。為了避免競爭,acquire方法必須首先表明它們需要一個信號,然后重試原子獲取,當失敗時,阻塞。
  2. CANCELLED=1:由於超時或中斷,該節點被取消。節點不會離開這個狀態。特別是,取消節點的線程不會再次阻塞。
  3. CONDITION=-2:此節點當前處於條件隊列中。在傳輸之前,它不會被用作同步隊列節點,此時狀態將被設置為0。
  4. PROPAGATE=-3:釋放的共享應該傳播到其他節點。在doReleaseShared中設置這個(僅針對頭節點),以確保傳播繼續,即使其他操作已經干預。
  5. waitStatus=0:以上都不是

根據上面代碼,知道AQS隊列是一個雙鏈表實現的隊列,每個節點包含prev指針和next指針,具體如下圖:
在這里插入圖片描述

問題:AQS內部類Node
回答:AQS本質是一個非循環的雙向鏈表(也可以稱為隊列),所以它是由一個個節點構成的,就是Node,后面的lock() unlock() await() signal()/signalAll()都是以Node為基本元素操作的。

問題:AQS類中的Node內部類中需要保存什么信息呢?
回答:一個六個,其中,prev、next 兩個Node類型,表示做指針,thread 存放節點的值,因為AQS隊列的節點就是存放線程的,所以這個值類型就是Thread,最后,nextWaiter也是Node類型,表示下一個等待節點, waitStatus表示當前節點等待狀態,SHARED|EXCLUSIVE 表示是獨占還是共享。

volatile int waitStatus;   //當前節點等待狀態
volatile Node prev;       //上一個節點
volatile Node next;         //下一個節點
volatile Thread thread;         //節點中的值
Node nextWaiter;        //下一個等待節點
 //指示節點共享還是獨占,默認初始是共享
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
復制代碼

記住一個Node節點的六個屬性(共享/獨占算一個),下面看源碼就輕松些

  1. 處在同步隊列中使用到的屬性(本文用:加鎖、解鎖)包括:next prev thread waitStatus,所以同步隊列是雙向非循環鏈表,涉及的類變量AbstractQueuedSynchronizer類中的head和tail,分別指向同步隊列中的頭結點和尾節點。
  2. 處在等待隊列中使用到的屬性(下一篇博文用:阻塞、喚醒)包括:nextWaiter thread waitStatus,所以等待隊列是單向非循環鏈表,涉及的類變量ConditionObject類中的firstWaiter和lastWaiter,分別指向等待隊列中的頭結點和尾節點。
  3. AQS隊列是工作隊列、同步隊列,是非循環雙向隊列:當使用到head tail的時候,就說AQS隊列建立起來了,單個線程不使用到head tail,所以AQS隊列沒有建立起來;
  4. 等待隊列,是非循環單向隊列:當使用firstWaiter lastWaiter的時候,就說等待隊列建立起來了。
  5. lock()和unlock()就是操作同步隊列:lock()將線程封裝到節點里面(此時,節點使用到的屬性是thread nextWaiter waitStatus),放到同步隊列,即AQS隊列中,unlock()將存放線程的節點從同步隊列中拿出來,表示這個線程工作完成。
  6. await()和signal()就是操作等待隊列:await()將線程封裝到節點里面(此時,節點使用到的屬性是thread prev next waitStatus),放到等待隊列里面,signal()從等待隊列中拿出元素。

2.2 公平鎖加鎖需要工作隊列:FairSync中的lock()方法(重點)

2.2.1 lock方法只有一個線程的情況

2.2.1.1 整體路程圖(第1次加鎖 + 第2~n次加鎖)

lock方法只有一個線程的情況(ps:此時還沒有AQS隊列,head==tail),如下圖所示:
在這里插入圖片描述

對於上圖的解釋,看這張圖的正確姿勢是:

  1. 上圖中涉及的方法包括 acquire() 、 tryAcquire() 、 hasQueuedPredecessors()方法;
  2. 對於acquire() 方法,就是直接調用 tryAcquire() 方法,實參傳入1。
  3. 對於tryAcquire()方法,由於只有一個線程A,第一次進入的時候要CAS加鎖(if判斷后半段)並將自己設置為獨占線程(if代碼段),所以兩行代碼,以后就是重入鎖了,直接進入,不用CAS操作和設置獨占鎖了,直接通過setState()修改state值,每次加一即可,因為實參acquires為1。
  4. 對於hasQueuedPredecessors()方法,該方法就是判斷AQS工作隊列是否建立起來了,這里h==t,所以,沒有建立起來,返回false。
  5. 總體流程,對於同一線程A,對於圖中唯一一個菱形判斷框,第一次進入的時候要CAS加鎖(if判斷后半段)並將自己設置為獨占線程(if代碼段);第2~n次進入就是重入鎖,直接進入,不用CAS操作和設置獨占鎖了,直接通過setState()修改state值,每次加一即可,這就是整體流程,線程A的整體流程,分為兩種球情況而已。

公平鎖加鎖流程(只有一個線程的時候):

  1. 直接調用tryAcquire,然后判斷state的是不是等於0
  2. 對於A線程第1次加鎖,等於0,證明是第一次加鎖,第一次加鎖hasQueuedPredecessors()一定通過(返回為false取反為true),通過CAS操作將state的值改成1,然后true,返回true表示加鎖成功,就完成了加鎖。
  3. 對於A線程第2~n次加鎖,不等於0,表示不是第一次加鎖,這個鎖是重入鎖,這個時候將原來的state值繼續通過CAS操作加上1,再次返回true,表示加鎖成功,就完成了加鎖。

tip1:需要注意的是,這個時候AQS的隊列沒有創建出來。
tip2:setExclusiveOwnerThread(current); // 這里是設置當前節點為獨占 記住上面六個屬性
tip3:看源碼的時候,知道自己在看什么,這里是看FairSync的lock()方法實現
tip4:源碼一般命名優美,可以從命名上來看,幫助理清思路,例如 lock()是加鎖、acquire()是去獲得tryAcquire() 是嘗試加鎖、acquireQueued()是獲得隊列

2.2.1.2 重要方法:tryAcquire()源碼解析

對於只有一個線程A使用lock.lock();加鎖,最重要的方法就是tryAcquire,就是這個方法,將線程A區分對待,第一次加鎖和非第一次加鎖,源碼拿出來講一講:

 protected final boolean tryAcquire(int acquires) {  // **1、tryAcquire是去獲取,2、返回為true就是使用獲取的方式加鎖成功(可以第一次,也可以是重入鎖)**
            final Thread current = Thread.currentThread();
            int c = getState();   // 當前狀態
            if (c == 0) {   // 當前狀態為0,就是默認狀態
                if (!hasQueuedPredecessors() &&    //  **1、hasQueuedPredecessors這個方法重要,下面解釋**
                    compareAndSetState(0, acquires)) {  // **1、只要上面那個hasQueuedPredecessors()返回為false,取反為true,這個cas一定是可以通過的,只是自旋等一下罷了**
                    setExclusiveOwnerThread(current);   // **1、設置當前線程為獨占線程,因為當前線程已經加鎖成功了,所以設置當前線程為互斥資源的獨占線程**
                    //**2、為什么說當前線程加鎖成功了,因為這里返回true啊**
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {  // 這句表示當前線程為獨占線程
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;   // **1、因為當前線程是獨占線程,所以一定是加鎖成功,這里返回true就好
                // 2、既然已經是獨占線程,就沒有必要再次設置當前線程為獨占線程了,直接返回true**
            }
            return false;   // **1、如果既不是第一次,也不是重入鎖,就不能通過獲取的方式去加鎖,要自己加鎖,這里返回false,加鎖失敗**
        }
復制代碼

2.2.1.3 重要方法:hasQueuedPredecessors()源碼解析

public final boolean hasQueuedPredecessors() {
    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());
}
復制代碼

問題:hasQueuedPredecessors()源碼解析?
回答:

  1. 如果h == t成立,h和t均為null或是同一個具體的節點,無后繼節點,方法返回false,表示要通過獲取鎖tryLock()加鎖,這種情況是第一個節點,類變量tail == head == null。
    key:如果類變量head==tail,表示沒有節點或只有一個節點,所以一定是沒有前驅節點的,方法直接返回false,不用多說,注意,后面的,head!=tail至少兩個節點

  2. 如果h!=t成立(至少兩個節點),而且 head.next == 為null,方法返回true。什么情況下h!=t的同時h.next==null??有其他線程第一次正在入隊時,可能會出現。見AQS的enq方法,compareAndSetHead(node)完成,還沒執行tail=head語句時,此時tail=null,head=newNode,head.next=null。
    key:此時,頭結點設置為新建節點,所以head=newNode
    但是,還未將頭結點設置為尾節點,所以tail=null,為默認值
    同時,這是第一次執行enq()方法,沒有設置 node.prev = t; 和 t.next = node;,所以head.next=null。

  3. 如果h!=t成立(當前類變量head 和 tail不是同一個,至少兩個節點),head.next != null,(true && (false || ))則判斷head.next是否是當前線程,如果是當前線程(true && (false || false)),方法返回false,key:表示要通過獲取鎖的方式加鎖;如果不是當前線程(true && (false || true))返回true,key:表示正在運行的線程不是當前的這個current,需要等待。
    (head節點是獲取到鎖的節點,但是任意時刻head節點可能占用着鎖,也可能釋放了鎖(unlock()),未被阻塞的head.next節點對應的線程在任意時刻都是有必要去嘗試獲取鎖)

實際上,hasQueuedPredecessors返回為true不通過,只需要等一段時間罷了(上面關於hasQueuedPredecessors方法的意義:如果該方法返回true,則表示有線程比當前線程更早地請求獲取鎖,因此需要等待前驅線程獲取並釋放鎖之后才能繼續獲取鎖)

問題:hasQueuedPredecessors()方法是用來干什么的?
回答:

  1. hasQueuedPredecessors()方法的意義:該方法表示對加入了同步隊列中當前節點是否有前驅節點的判斷,如果該方法返回true,則表示有線程比當前線程更早地請求獲取鎖,因此需要等待前驅線程獲取並釋放鎖之后才能繼續獲取鎖,因為是公平鎖,要按照隊列來。
  2. hasQueuedPredecessors()方法的調用以及返回的意義:hasQueuedPredecessors()方法只在tryAcquire()方法里面被調用執行過,hasQueuedPredecessors()返回false表示要嘗試通過獲取鎖的方式加鎖,沒有前驅節點請求獲取鎖,返回為true表示不需要通過獲取鎖的方式加鎖,已經有前驅節點請求獲取了。

問題:如何進入hasQueuedPredecessors()方法?
回答:lock() -> acquire() -> tryAcquire() -> hasQueuedPredecessors()

2.2.2 lock方法中有兩個線程的情況

2.2.2.1 整體路程圖(線程A加鎖 + 線程B加鎖)

lock方法實現的公平鎖AQS隊列中有兩個線程的情況,如下圖所示:

在這里插入圖片描述

上圖注意兩點:

  1. 流程:兩個線程的運行,左邊是線程A先獲得鎖,然后線程B再去嘗試獲得鎖,但是線程B一定要等到線程A釋放鎖之后,才能獲得鎖成功,在這之前只能阻塞,即源碼中for循環;
  2. 仔細對比兩個圖,和上一次的改變是:線程B節點的上一個節點的waitStatus的值從0修改成-1。

對於上圖的解釋:

先是線程A加鎖,然后線程B加鎖,線程A加鎖沒必要說,和上面一個線程的情況一樣,線程B加鎖分為兩種情況:線程A還沒有解鎖,線程B加鎖失敗;線程A已經解鎖,線程B CAS操作加鎖成功。

2.2.2.2 第一種情況:當線程B進來的時候,死循環中線程A沒有解鎖

第一,建立AQS隊列

我們假設線程A直接獲取到了鎖(獲取鎖的過程和上面單線程一樣,不再贅言),但是線程A還沒有解鎖,這個時候線程B來進行加鎖,走來會執行tryAcquire()方法,這個時候線程A沒有解鎖,所以這個tryAcquire()方法會直接返回false(state!=0,也不是重入鎖),然后會調用addWaiter(Node.EXCLUSIVE)方法(addWaiter()是新方法:上面一個線程的時候沒有涉及到,這里要重點分析),這個時候會在這個方法中的enq(node)的方法中初始化AQS隊列,也會利用尾插法將當前的節點插入到AQS隊列中去。AQS隊列如下圖所示:

在這里插入圖片描述

對於當前的AQS隊列解釋:

  1. 這時AQS隊列新建起來了
  2. head節點中thread=null表示沒有存放任何線程,waitStatus=0表示什么都不是
  3. tail節點中thread=線程B表示存放的是線程B,waitStatus=0表示什么都不是

第二,addWaiter()方法中調用enq()方法,新建AQS隊列

完成AQS隊列的方法是addWaiter()中調用的enq()方法,且看addWaiter()方法和enq()方法

 private Node addWaiter(Node mode) {  // **1、實際參數是Node.EXCLUSIVE,就是當前獨占節點,表示下一個等待節點就是正在獨占的那個線程的節點,因為它釋放鎖就要到插入了,所以這個方法稱為addWaiter,意為添加下一個等待節點**
        Node node = new Node(Thread.currentThread(), mode);  // 新建一個節點,存放當前線程,當前線程為內容,實參為下一個等待節點nextWaiter
        Node pred = tail;   // 當前尾節點賦值,當前tail==null
        if (pred != null) {
            node.prev = pred;   // 如果不為空,進來,新建節點的前一個是之前的尾節點,就是尾插法
            if (compareAndSetTail(pred, node)) {   // 設置新的尾節點,從之前的尾節點pred到現在的node
                pred.next = node;     // 之前尾節點的next設置為這個節點
                // **由此可知,尾插法三步驟:設置新節點的prev為之前尾節點、重新設置tail類變量的指向、設置之前尾節點的next為新建節點(就是三個Node類型指針而已,很簡單)**
                return node;   // 返回新建節點
            }
        }
        enq(node);   // 返回值沒有接收者,但是隊列新建好了
        return node;   // 返回這個新建的節點
    }
private Node enq(final Node node) {
        for ( ; ; ) {    // **1、死循環,不創建好AQS隊列不退出**
            Node t = tail;
            if (t == null) { // Must initialize  **1、第一次進入,必須初始化,這里表示連尾節點都沒有**
                if (compareAndSetHead(new Node()))   // **for+if(cas)就是線程同步**
                    tail = head;    // **1、新建一個節點,設置為頭結點,因為只有一個節點,所以尾節點也是這個節點**
            } else {
                node.prev = t; //  **1、這是時候,第二次循環,因為head tail都是新節點,第二次循環中使用 Node t = tail;將t設置為這個新節點**
                if (compareAndSetTail(t, node)) {   // 方法名是compareAndSetTail,表示設置尾節點,自旋,知道設置成功  for+if(cas)就是線程同步,設置tail類變量,將tail從t變為node,所以傳入參數node是尾節點
                    t.next = node;   // 尾節點指向參數node,頭結點還是指向t
                    //  **由此可知,尾插法三步驟:設置參數節點的prev為之前尾節點t、重新設置tail類變量的指向從之前的t到參數節點node、設置之前尾節點t的next為參數節點node(就是三個Node類型指針而已,很簡單),最后隊列兩個元素 t 和 node**
                    return t;   // 返回頭結點t
                }
            }
        }
    }
復制代碼

tip:head和tail是類變量,類似指針,指向其他節點
compareAndSetTail(t, node) // 設置tail類變量,將tail從t變為node,所以傳入參數node是尾節點
compareAndSetHead(t, node) // 設置tail類變量,將head從t變為node,所以傳入參數node是頭節點
compareAndSetState(0, acquires) // 設置state類變量,從0到1,cas保證安全

在這里插入圖片描述

第三,acquireQueued()方法接收addWaiter()返回值,調用shouldParkAfterFailedAcquire()方法,修改AQS隊列中節點的waitStatus值從0到1

方法addWaiter()返回當前的節點,然后調用acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法(tip:返回的剛剛在addWaiter()方法中新建的最尾巴的節點作為acquireQueued方法的參數,arg參數是1,傳遞過來的)。這個方法中是一個死循環,由於線程A沒有釋放鎖(tryAcquire()方法會直接返回false(state!=0,也不是重入鎖)),會執行shouldParkAfterFailedAcquire(p, node)(p表示線程B節點的上一個節點,p = node.predecessor();就是最尾巴節點上一個,node表示線程B的節點,就是addWaiter()方法中新建的最尾巴的節點)第一次進這個方法會將線程B節點的上一個節點的waitStatus的值改成-1(執行最后一個else compareAndSetWaitStatus(pred, ws, Node.SIGNAL);),然后返回false,這個時候的AQS隊列如下圖:

在這里插入圖片描述
tip:仔細對比兩個圖,和上一次的改變是:線程B節點的上一個節點的waitStatus的值從0修改成-1。

  1. 第一次進入shouldParkAfterFailedAcquire()方法會將線程B節點的上一個節點的waitStatus的值改成-1,然后返回false。此時的AQS工作隊列:head節點thread==null表示沒有存放線程,waitStatus=-1表示后面節點的線程需要釋放;tail節點thread=線程B表示存放的是線程B,waitStatus=0表示什么都不是。

  2. 第二次進入shouldParkAfterFailedAcquire()方法的時候,會返回true( if (ws == Node.SIGNAL) return true;),會執行后面的方法parkAndCheckInterrupt()( LockSupport.park(this);),這個時候線程B就會被park在這。(1、直到線程A解鎖了,第二種情況可以當做第一個情況后面的執行來看)

2.2.2.3 第二種情況:死循環中線程A已經解鎖了

上面的情況都是在線程A沒有解鎖的時候,如果在死循環中線程A已經解鎖了。這個時候判斷線程B節點的上一個節點是不是頭結點,如果是的話,直接執行tryAcquire(),將當前線程B設置成獨占線程,同時將state的值通過CAS操作設置成1,如果成功的話,直接返回true。表示加鎖成功。這個時候會執行這個if判斷中代碼。執行setHead(node),這個時候AQS隊列如下圖:

在這里插入圖片描述

 if (p == head && tryAcquire(arg)) {
    setHead(node);      //  node就是addWaiter的尾巴節點,
    p.next = null; // help GC 看Java四種引用就知道  前面那個節點的next設置為null 
    failed = false;   // 局部變量failed初始為true,要下去執行cancelAcquire,這里設置為false,不執行cancelAcquire了
    return interrupted;   // false
 }
 private void setHead(Node node) {
    head = node;   // 類變量head指向addWaiter的尾巴節點
    node.thread = null;  // 這個節點thread=null
    node.prev = null;   // 這個節點prev==null  因為要變成頭結點,非循環雙向鏈表,所以前驅指針為null 
}
復制代碼

這個時候原來的線程B節點出隊列(因為B節點要去執行了),然后永遠會維護一個頭結點中thread為null的AQS隊列。

2.2.3 lock方法中有三個線程的情況

lock方法中有三個線程情況,如下圖:

在這里插入圖片描述

三個線程和兩個線程的情況是差不多的,即加鎖成功的節點永遠是頭結點的下一個節點中的線程加鎖成功,因為是公平鎖。

2.3 非公平鎖加鎖不需要隊列:NonfairSync類的lock()方法(了解即可,搞懂了公平鎖的lock(),這個就好懂了,然后只要知道公平鎖lock()和非公平鎖lock()區別就好)

非公平鎖加鎖流程:

  1. 非公平鎖會走來直接嘗試加鎖;
  2. 如果加鎖成功,直接執行線程中的代碼;
  3. 如果加鎖不成功,直接走公平鎖的邏輯。

2.4 其他加鎖方法:ReentrantLock類的tryLock()方法(了解即可,和上面公平鎖lock()大同小異)

tryLock()方法和lock()方法是差不多,tryLock方法,嘗試加鎖不成功后就直接返回false,具體的代碼如下:

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

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;
}
復制代碼

2.5 其他加鎖方法:ReentrantLock類的tryLock(long timeout, TimeUnit unit)方法(了解即可,和上面公平鎖lock()大同小異)

  1. tryLock(long timeout, TimeUnit unit)方法,加了一個獲取鎖的時間,如果這個時間內沒有獲取到鎖,直接返回false,表示加鎖失敗;如果在這個時間內調用tryAcquire(arg)獲得到鎖,表示加鎖成功,tryAcquireNanos(int arg, long nanosTimeout)方法返回值直接為true,即tryLock(long timeout, TimeUnit unit)方法返回值為true。

  2. 如果tryAcquire(arg)返回為false,會執行doAcquireNanos(arg, nanosTimeout)方法,走來先將當前節點用尾插法的方式插入到AQS隊列中去,如果AQS隊列沒有初始化,直接初始化,將當前的節點放入到尾結點中去。然后進入死循環,這個時候判斷當前節點的上一個節點是不是頭結點,再次嘗試加鎖,如果成功直接返回true,如果失敗將當前的節點的線程直接park指定的時間,當時間到了直接喚醒。再次嘗試獲取鎖,如果成功直接返回true,如果失敗直接返回false,這個方法中是可以直接響應中斷的。

詳細:

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

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();    // 這里會立即響應中斷
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}

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 GC
                failed = false;
                return true;
            }
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L)
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);   // park指定的時間
            if (Thread.interrupted())
                throw new InterruptedException();  // 有中斷立即響應
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
復制代碼

2.6 其他加鎖方法:ReentrantLock類的lockInterruptibly()方法(了解即可,和上面公平鎖lock()大同小異)

lockInterruptibly和lock的區別:

  1. lockInterruptibly是會立即響應中斷的:並且在park中的線程也會interruptibly喚醒的,因為這個時候返回true,直接拋出異常,響應對應的中斷。
  2. lock是要等線程執行完才會響應中斷:是因為park中線程被中斷喚醒后,沒有拋出異常,只是將中斷的標志設置成了true,等到獲取到鎖,執行完,才會響應對象的中斷。

lockInterruptibly是會立即響應中斷的(源碼解釋)

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

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

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 GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();    // 有中斷立即響應
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
復制代碼

lock是要等線程執行完才會響應中斷(源碼解釋)

final void lock() {
    acquire(1);
}

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;    // 沒有立即響應中斷,僅僅設置一個標志位interrupt=true
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
復制代碼

三、AQS源碼對於lock.unlock()的實現

講完了加鎖的過程,我們再來看看解鎖的過程,即ReentrantLock類的unlock()方法。

3.1 解鎖整體流程圖

在這里插入圖片描述

3.2 解鎖涉及的函數:unlock()->release()->tryRelease() + 解鎖三種情況

解鎖涉及的函數:unlock()->release()->tryRelease()

public void unlock() {
    sync.release(1);
}

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        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;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}
復制代碼

線程解鎖的三種情況:

  1. 當前線程不在AQS隊列中,執行tryRelease()方法。
    如果當前線程不是重入鎖(即滿足 if(c==0)為true),直接將當前的線程獨占標識去除掉,然后將state的值通過CAS的操作改成0;
    如果當前線程加的是重入鎖(即滿足 if(c!=0)為false),解鎖一次,state的值減1,如果state的值是等於0的時候,返回true。表示解鎖成功。

  2. AQS隊列中只有一個頭結點,這個時候tryRelease()返回的結果和上面的情況是一樣的。這個時候返回的true,會進當前的if中去,然后判斷頭結點是不是為null和頭結點中waitStatus的值是不是等於0。這個時候head不等於null,但是waitState是等於0,if判斷不成立,不會執行unpark的方法。會直接返回true。表示解鎖成功。

  3. AQS隊列中不止一個頭結點,還有其他節點,這個時候tryRelease()返回的結果和上面的情況是一樣的。這個時候返回的true,會進當前的if中去,然后判斷頭結點是不是為null和頭結點中waitStatus的值是不是等於0。這個時候head不等於null,但是waitState是等於-1,if判斷成立,會執行unpark的方法。unpark方法中會unpark頭結點的下一個節點,然后如果當前的節點的狀態是取消的狀態,會從最后一個節點開始找,找到當前節點的下一個不是取消狀態的節點進行unpark。這個時候也會直接返回true。表示解鎖成功。

3.3 重要函數:unparkSuccessor()(在release()中被調用)

private void unparkSuccessor(Node node) {  // 同步隊列中的頭結點head傳遞過來
     int ws = node.waitStatus;  // 為 -1 
     if (ws < 0)
         compareAndSetWaitStatus(node, ws, 0);  // 設置狀態 node的ws變為0
     Node s = node.next;   // 找到工作隊列的head的后面一個節點
     if (s == null || s.waitStatus > 0) {  // head后面這個節點為空,或者waitStatus大於0
         s = null;    // 如果是因為waitStatus大於0而進入這個if,設置head后面的這個節點為null
         for (Node t = tail; t != null && t != node; t = t.prev)  // 從尾巴開始遍歷,布局變量為t  沒有遍歷完或沒找就繼續找
             if (t.waitStatus <= 0)   // 如果t.waitStatus <= 0,將這個t記錄到s,后面unpark用   找到當前節點的下一個不是取消狀態的節點進行unpark
                 s = t;   
     }
     if (s != null)   // head后面的節點不為空,直接對head后面這個節點unpark  畢竟公平鎖
         LockSupport.unpark(s.thread);  
 }
復制代碼

問題:第二個if是如何判斷三情況的 if (h != null && h.waitStatus != 0) ?
回答:

  1. 第一種情況,沒有AQS隊列,head一定為null,就是return true;
  2. 第二種情況,有AQS隊列但是同步隊列中只有一個節點,head不為null,但是唯一的一個節點既是頭結點也是尾節點,所有h.waitStatus == 0 ,不滿足條件,就是return true;
  3. 第三種情況,有AQS隊列但是同步隊列中超過一個節點,head不為null,頭結點和尾節點是不同節點,所以h.waitStatus==-1,滿足第二層if判斷,就是 unparkSuccessor(h); 然后 return true;

小結:雖然都是返回為true,解鎖成功,但是內部邏輯是不同的。

兩種重要問題:

  1. 問題:為什么需要同步隊列AQS?
    回答:同步隊列是lock公平鎖的內部數據結構,非公平鎖不需要同步隊列。
  2. 問題:為什么同步隊列是非循環雙鏈表,非循環單鏈表不行嗎?
    回答:公平鎖中,lock.lock()使用尾插法插入,但是,在調用lock.unlock()方法的時候,由於頭節點是成功獲取到同步狀態的節點,而頭節點的線程釋放了同步狀態后,將會喚醒其他后續節點s,后繼節點的線程被喚醒后需要檢查自己的前驅節點是否是頭節點,如果是則嘗試獲取同步狀態。所以為了能讓后繼節點獲取到其前驅節點,同步隊列便設置為雙向鏈表,而等待隊列沒有這樣的需求,就為單鏈表。

四、面試金手指(ReentrantLock中lock()與unlock(),即AQS加鎖解鎖)

面試問題:lock機制是如何實現公平鎖的加鎖和解鎖的(因為synchronized無法實現公平鎖)
回答:下面4.1 4.2 4.3

4.1 先搞懂AQS類中的基本元素Node節點

問題:AQS內部類Node
回答:AQS本質是一個非循環的雙向鏈表(也可以稱為隊列),所以它是由一個個節點構成的,就是Node,后面的lock() unlock() await() signal()/signalAll()都是以Node為基本元素操作的。

問題:AQS類中的Node內部類中需要保存什么信息呢?
回答:一個六個,其中,prev、next 兩個Node類型,表示做指針,thread 存放節點的值,因為AQS隊列的節點就是存放線程的,所以這個值類型就是Thread,最后,nextWaiter也是Node類型,表示下一個等待節點, waitStatus表示當前節點等待狀態,SHARED|EXCLUSIVE 表示是獨占還是共享。

volatile int waitStatus;   //當前節點等待狀態
volatile Node prev;       //上一個節點
volatile Node next;         //下一個節點
volatile Thread thread;         //節點中的值
Node nextWaiter;        //下一個等待節點
 //指示節點共享還是獨占,默認初始是共享
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
復制代碼

記住一個Node節點的六個屬性(共享/獨占算一個),下面看源碼就輕松些

  1. 處在同步隊列中使用到的屬性(本文用:加鎖、解鎖)包括:next prev thread waitStatus,所以同步隊列是雙向非循環鏈表,涉及的類變量AbstractQueuedSynchronizer類中的head和tail,分別指向同步隊列中的頭結點和尾節點。
  2. 處在等待隊列中使用到的屬性(下一篇博文用:阻塞、喚醒)包括:nextWaiter thread waitStatus,所以等待隊列是單向非循環鏈表,涉及的類變量ConditionObject類中的firstWaiter和lastWaiter,分別指向等待隊列中的頭結點和尾節點。
  3. AQS隊列是工作隊列、同步隊列,是非循環雙向隊列:當使用到head tail的時候,就說AQS隊列建立起來了,單個線程不使用到head tail,所以AQS隊列沒有建立起來;
  4. 等待隊列,是非循環單向隊列:當使用firstWaiter lastWaiter的時候,就說等待隊列建立起來了。
  5. lock()和unlock()就是操作同步隊列:lock()將線程封裝到節點里面(此時,節點使用到的屬性是thread nextWaiter waitStatus),放到同步隊列,即AQS隊列中,unlock()將存放線程的節點從同步隊列中拿出來,表示這個線程工作完成。
  6. await()和signal()就是操作等待隊列:await()將線程封裝到節點里面(此時,節點使用到的屬性是thread prev next waitStatus),放到等待隊列里面,signal()從等待隊列中拿出元素。

問題:為什么負責同步隊列的head和tail在AbstractQueuedSynchronizer類中,但是負責等待隊列的firstWaiter和lastWaiter在ConditionObject類中?

回答:

  1. 線程同步互斥是直接通過ReentrantLock類對象 lock.lock() lock.unlock()實現的,而ReentrantLock類對象是調用AQS類實現加鎖解鎖的,所以負責同步隊列的head和tail在AbstractQueuedSynchronizer類中;
  2. 線程阻塞和喚醒是通過ReentrantLock類對象lock.newCondition()得到一個對應,condition引用指向這個對象,然后condition.await() condition.signal()實現的,所以負責等待隊列的firstWaiter和lastWaiter在ConditionObject類中。

4.2 公平鎖加鎖

4.2.1 單線程加鎖過程

4.2.1.1 整體路程圖(第1次加鎖 + 第2~n次加鎖)

公平鎖加鎖流程(只有一個線程的時候):

  1. 直接調用tryAcquire,然后判斷state的是不是等於0
  2. 對於A線程第1次加鎖,等於0,證明是第一次加鎖,第一次加鎖hasQueuedPredecessors()一定通過(返回為false取反為true),通過CAS操作將state的值改成1,然后true,返回true表示加鎖成功,就完成了加鎖。
  3. 對於A線程第2~n次加鎖,不等於0,表示不是第一次加鎖,這個鎖是重入鎖,這個時候將原來的state值繼續通過CAS操作加上1,再次返回true,表示加鎖成功,就完成了加鎖。

tip1:需要注意的是,這個時候AQS的隊列沒有創建出來。
tip2:setExclusiveOwnerThread(current); // 這里是設置當前節點為獨占
tip3:看源碼的時候,知道自己在看什么,這里是看FairSync的lock()方法實現
tip4:源碼一般命名優美,可以從命名上來看,幫助理清思路,例如 lock()是加鎖、acquire()是去獲得tryAcquire() 是嘗試加鎖、acquireQueued()是獲得隊列

4.2.1.2 重要方法:tryAcquire()源碼解析

對於只有一個線程A使用lock.lock();加鎖,最重要的方法就是tryAcquire,就是這個方法,將線程A區分對待,第一次加鎖和非第一次加鎖,源碼拿出來講一講:

 protected final boolean tryAcquire(int acquires) {  // **1、tryAcquire是去獲取,2、返回為true就是使用獲取的方式加鎖成功(可以第一次,也可以是重入鎖)**
            final Thread current = Thread.currentThread();
            int c = getState();   // 當前狀態
            if (c == 0) {   // 當前狀態為0,就是默認狀態
                if (!hasQueuedPredecessors() &&    //  **1、hasQueuedPredecessors這個方法重要,下面解釋**
                    compareAndSetState(0, acquires)) {  // **1、只要上面那個hasQueuedPredecessors()返回為false,取反為true,這個cas一定是可以通過的,只是自旋等一下罷了**
                    setExclusiveOwnerThread(current);   // **1、設置當前線程為獨占線程,因為當前線程已經加鎖成功了,所以設置當前線程為互斥資源的獨占線程**
                    //**2、為什么說當前線程加鎖成功了,因為這里返回true啊**
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {  // 這句表示當前線程為獨占線程
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;   // **1、因為當前線程是獨占線程,所以一定是加鎖成功,這里返回true就好
                // 2、既然已經是獨占線程,就沒有必要再次設置當前線程為獨占線程了,直接返回true**
            }
            return false;   // **1、如果既不是第一次,也不是重入鎖,就不能通過獲取的方式去加鎖,要自己加鎖,這里返回false,加鎖失敗**
        }
復制代碼

4.2.1.3 重要方法:hasQueuedPredecessors()源碼解析

問題:hasQueuedPredecessors()源碼解析?
回答:

  1. 如果h == t成立,h和t均為null或是同一個具體的節點,無后繼節點,方法返回false,表示要通過獲取鎖tryLock()加鎖,這種情況是第一個節點,類變量tail == head == null。
    key:如果類變量head==tail,表示沒有節點或只有一個節點,所以一定是沒有前驅節點的,方法直接返回false,不用多說,注意,后面的,head!=tail至少兩個節點

  2. 如果h!=t成立(至少兩個節點),而且 head.next == 為null,方法返回true。什么情況下h!=t的同時h.next==null??有其他線程第一次正在入隊時,可能會出現。見AQS的enq方法,compareAndSetHead(node)完成,還沒執行tail=head語句時,此時tail=null,head=newNode,head.next=null。
    key:此時,頭結點設置為新建節點,所以head=newNode
    但是,還未將頭結點設置為尾節點,所以tail=null,為默認值
    同時,這是第一次執行enq()方法,沒有設置 node.prev = t; 和 t.next = node;,所以head.next=null。

  3. 如果h!=t成立(當前類變量head 和 tail不是同一個,至少兩個節點),head.next != null,(true && (false || ))則判斷head.next是否是當前線程,如果是當前線程(true && (false || false)),方法返回false,key:表示要通過獲取鎖的方式加鎖;如果不是當前線程(true && (false || true))返回true,key:表示正在運行的線程不是當前的這個current,需要等待。
    (head節點是獲取到鎖的節點,但是任意時刻head節點可能占用着鎖,也可能釋放了鎖(unlock()),未被阻塞的head.next節點對應的線程在任意時刻都是有必要去嘗試獲取鎖)

實際上,hasQueuedPredecessors返回為true不通過,只需要等一段時間罷了(上面關於hasQueuedPredecessors方法的意義:如果該方法返回true,則表示有線程比當前線程更早地請求獲取鎖,因此需要等待前驅線程獲取並釋放鎖之后才能繼續獲取鎖)

問題:hasQueuedPredecessors()方法是用來干什么的?
回答:

  1. hasQueuedPredecessors()方法的意義:該方法表示對加入了同步隊列中當前節點是否有前驅節點的判斷,如果該方法返回true,則表示有線程比當前線程更早地請求獲取鎖,因此需要等待前驅線程獲取並釋放鎖之后才能繼續獲取鎖,因為是公平鎖,要按照隊列來。
  2. hasQueuedPredecessors()方法的調用以及返回的意義:hasQueuedPredecessors()方法只在tryAcquire()方法里面被調用執行過,hasQueuedPredecessors()返回false表示要嘗試通過獲取鎖的方式加鎖,沒有前驅節點請求獲取鎖,返回為true表示不需要通過獲取鎖的方式加鎖,已經有前驅節點請求獲取了。

問題:如何進入hasQueuedPredecessors()方法?
回答:lock() -> acquire() -> tryAcquire() -> hasQueuedPredecessors()

4.2.2 兩個線程加鎖過程(A線程 B線程)

4.2.2.1 第一種情況:當線程B進來的時候,死循環中線程A沒有解鎖

第一,建立AQS隊列

我們假設線程A直接獲取到了鎖(獲取鎖的過程和上面單線程一樣,不再贅言),但是線程A還沒有解鎖,這個時候線程B來進行加鎖,走來會執行tryAcquire()方法,這個時候線程A沒有解鎖,所以這個tryAcquire()方法會直接返回false(state!=0,也不是重入鎖),然后會調用addWaiter(Node.EXCLUSIVE)方法(addWaiter()是新方法:上面一個線程的時候沒有涉及到,這里要重點分析),這個時候會在這個方法中的enq(node)的方法中初始化AQS隊列,也會利用尾插法將當前的節點插入到AQS隊列中去。AQS隊列如下圖所示:

在這里插入圖片描述

對於當前的AQS隊列解釋:

  1. 這時AQS隊列新建起來了
  2. head節點中thread=null表示沒有存放任何線程,waitStatus=0表示什么都不是
  3. tail節點中thread=線程B表示存放的是線程B,waitStatus=0表示什么都不是

第二,addWaiter()方法中調用enq()方法,新建AQS隊列

完成AQS隊列的方法是addWaiter()中調用的enq()方法,且看addWaiter()方法和enq()方法

 private Node addWaiter(Node mode) {  // **1、實際參數是Node.EXCLUSIVE,就是當前獨占節點,表示下一個等待節點就是正在獨占的那個線程的節點,因為它釋放鎖就要到插入了,所以這個方法稱為addWaiter,意為添加下一個等待節點**
        Node node = new Node(Thread.currentThread(), mode);  // 新建一個節點,存放當前線程,當前線程為內容,實參為下一個等待節點nextWaiter
        Node pred = tail;   // 當前尾節點賦值,當前tail==null
        if (pred != null) {
            node.prev = pred;   // 如果不為空,進來,新建節點的前一個是之前的尾節點,就是尾插法
            if (compareAndSetTail(pred, node)) {   // 設置新的尾節點,從之前的尾節點pred到現在的node
                pred.next = node;     // 之前尾節點的next設置為這個節點
                // **由此可知,尾插法三步驟:設置新節點的prev為之前尾節點、重新設置tail類變量的指向、設置之前尾節點的next為新建節點(就是三個Node類型指針而已,很簡單)**
                return node;   // 返回新建節點
            }
        }
        enq(node);   // 返回值沒有接收者,但是隊列新建好了
        return node;   // 返回這個新建的節點
    }
private Node enq(final Node node) {
        for ( ; ; ) {    // **1、死循環,不創建好AQS隊列不退出**
            Node t = tail;
            if (t == null) { // Must initialize  **1、第一次進入,必須初始化,這里表示連尾節點都沒有**
                if (compareAndSetHead(new Node()))   // **for+if(cas)就是線程同步**
                    tail = head;    // **1、新建一個節點,設置為頭結點,因為只有一個節點,所以尾節點也是這個節點**
            } else {
                node.prev = t; //  **1、這是時候,第二次循環,因為head tail都是新節點,第二次循環中使用 Node t = tail;將t設置為這個新節點**
                if (compareAndSetTail(t, node)) {   // 方法名是compareAndSetTail,表示設置尾節點,自旋,知道設置成功  for+if(cas)就是線程同步,設置tail類變量,將tail從t變為node,所以傳入參數node是尾節點
                    t.next = node;   // 尾節點指向參數node,頭結點還是指向t
                    //  **由此可知,尾插法三步驟:設置參數節點的prev為之前尾節點t、重新設置tail類變量的指向從之前的t到參數節點node、設置之前尾節點t的next為參數節點node(就是三個Node類型指針而已,很簡單),最后隊列兩個元素 t 和 node**
                    return t;   // 返回頭結點t
                }
            }
        }
    }
復制代碼

tip:head和tail是類變量,類似指針,指向其他節點
compareAndSetTail(t, node) // 設置tail類變量,將tail從t變為node,所以傳入參數node是尾節點
compareAndSetHead(t, node) // 設置tail類變量,將head從t變為node,所以傳入參數node是頭節點
compareAndSetState(0, acquires) // 設置state類變量,從0到1,cas保證安全

在這里插入圖片描述

第三,acquireQueued()方法接收addWaiter()返回值,調用shouldParkAfterFailedAcquire()方法,修改AQS隊列中節點的waitStatus值從0到1

方法addWaiter()返回當前的節點,然后調用acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法(tip:返回的剛剛在addWaiter()方法中新建的最尾巴的節點作為acquireQueued方法的參數,arg參數是1,傳遞過來的)。這個方法中是一個死循環,由於線程A沒有釋放鎖(tryAcquire()方法會直接返回false(state!=0,也不是重入鎖)),會執行shouldParkAfterFailedAcquire(p, node)(p表示線程B節點的上一個節點,p = node.predecessor();就是最尾巴節點上一個,node表示線程B的節點,就是addWaiter()方法中新建的最尾巴的節點)第一次進這個方法會將線程B節點的上一個節點的waitStatus的值改成-1(執行最后一個else compareAndSetWaitStatus(pred, ws, Node.SIGNAL);),然后返回false,這個時候的AQS隊列如下圖:

在這里插入圖片描述
tip:仔細對比兩個圖,和上一次的改變是:線程B節點的上一個節點的waitStatus的值從0修改成-1。

  1. 第一次進入shouldParkAfterFailedAcquire()方法會將線程B節點的上一個節點的waitStatus的值改成-1,然后返回false。此時的AQS工作隊列:head節點thread==null表示沒有存放線程,waitStatus=-1表示后面節點的線程需要釋放;tail節點thread=線程B表示存放的是線程B,waitStatus=0表示什么都不是。

  2. 第二次進入shouldParkAfterFailedAcquire()方法的時候,會返回true( if (ws == Node.SIGNAL) return true;),會執行后面的方法parkAndCheckInterrupt()( LockSupport.park(this);),這個時候線程B就會被park在這。(1、直到線程A解鎖了,第二種情況可以當做第一個情況后面的執行來看)

4.2.2.2 第二種情況:當線程B進來的時候,死循環中線程A已經解鎖

上面的情況都是在線程A沒有解鎖的時候,如果在死循環中線程A已經解鎖了。這個時候判斷線程B節點的上一個節點是不是頭結點,如果是的話,直接執行tryAcquire(),將當前線程B設置成獨占線程,同時將state的值通過CAS操作設置成1,如果成功的話,直接返回true。表示加鎖成功。這個時候會執行這個if判斷中代碼。執行setHead(node),這個時候AQS隊列如下圖:

在這里插入圖片描述

 if (p == head && tryAcquire(arg)) {
    setHead(node);      //  node就是addWaiter的尾巴節點,
    p.next = null; // help GC 看Java四種引用就知道  前面那個節點的next設置為null 
    failed = false;   // 局部變量failed初始為true,要下去執行cancelAcquire,這里設置為false,不執行cancelAcquire了
    return interrupted;   // false
 }
 private void setHead(Node node) {
    head = node;   // 類變量head指向addWaiter的尾巴節點
    node.thread = null;  // 這個節點thread=null
    node.prev = null;   // 這個節點prev==null  因為要變成頭結點,非循環雙向鏈表,所以前驅指針為null 
}
復制代碼

這個時候原來的線程B節點出隊列(因為B節點要去執行了),然后永遠會維護一個頭結點中thread為null的AQS隊列。

4.3 lock.unlock()解鎖過程

線程解鎖的三種情況:

  1. 當前線程不在AQS隊列中,執行tryRelease()方法。
    如果當前線程不是重入鎖(即滿足 if(c==0)為true),直接將當前的線程獨占標識去除掉,然后將state的值通過CAS的操作改成0;
    如果當前線程加的是重入鎖(即滿足 if(c!=0)為false),解鎖一次,state的值減1,如果state的值是等於0的時候,返回true。表示解鎖成功。

  2. AQS隊列中只有一個頭結點,這個時候tryRelease()返回的結果和上面的情況是一樣的。這個時候返回的true,會進當前的if中去,然后判斷頭結點是不是為null和頭結點中waitStatus的值是不是等於0。這個時候head不等於null,但是waitState是等於0,if判斷不成立,不會執行unpark的方法。會直接返回true。表示解鎖成功。

  3. AQS隊列中不止一個頭結點,還有其他節點,這個時候tryRelease()返回的結果和上面的情況是一樣的。這個時候返回的true,會進當前的if中去,然后判斷頭結點是不是為null和頭結點中waitStatus的值是不是等於0。這個時候head不等於null,但是waitState是等於-1,if判斷成立,會執行unpark的方法。unpark方法中會unpark頭結點的下一個節點,然后如果當前的節點的狀態是取消的狀態,會從最后一個節點開始找,找到當前節點的下一個不是取消狀態的節點進行unpark。這個時候也會直接返回true。表示解鎖成功。

問題:第二個if是如何判斷三情況的 if (h != null && h.waitStatus != 0) ?
回答:

  1. 第一種情況,沒有AQS隊列,head一定為null,就是return true;
  2. 第二種情況,有AQS隊列但是同步隊列中只有一個節點,head不為null,但是唯一的一個節點既是頭結點也是尾節點,所有h.waitStatus == 0 ,不滿足條件,就是return true;
  3. 第三種情況,有AQS隊列但是同步隊列中超過一個節點,head不為null,頭結點和尾節點是不同節點,所以h.waitStatus==-1,滿足第二層if判斷,就是 unparkSuccessor(h); 然后 return true;

小結:雖然都是返回為true,解鎖成功,但是內部邏輯是不同的。

兩種重要問題:

  1. 問題:為什么需要同步隊列AQS?
    回答:同步隊列是lock公平鎖的內部數據結構,非公平鎖不需要同步隊列。
  2. 問題:為什么同步隊列是非循環雙鏈表,非循環單鏈表不行嗎?
    回答:公平鎖中,lock.lock()使用尾插法插入,但是,在調用lock.unlock()方法的時候,由於頭節點是成功獲取到同步狀態的節點,而頭節點的線程釋放了同步狀態后,將會喚醒其他后續節點s,后繼節點的線程被喚醒后需要檢查自己的前驅節點是否是頭節點,如果是則嘗試獲取同步狀態。所以為了能讓后繼節點獲取到其前驅節點,同步隊列便設置為雙向鏈表,而等待隊列沒有這樣的需求,就為單鏈表。

五、小結

ReentrantLock中lock()、unlock() 全解析,完成了。

天天打碼,天天進步!!!


作者:毛毛的學習筆記
鏈接:https://juejin.im/post/6893438433826439182
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

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



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