1. 程式人生 > >Java多執行緒程式設計-(12)-Java中的佇列同步器AQS和ReentrantLock鎖原理簡要分析

Java多執行緒程式設計-(12)-Java中的佇列同步器AQS和ReentrantLock鎖原理簡要分析

原文出自 : https://blog.csdn.net/xlgen157387/article/details/78341626



一、Lock介面

在上一篇文章中: Java多執行緒程式設計-(5)-使用Lock物件實現同步以及執行緒間通訊 介紹瞭如何使用Lock實現和synchronized關鍵字類似的同步功能,只是Lock在使用時需要顯式地獲取和釋放鎖,synchronized實現的隱式的獲取所和釋放鎖。

雖然Lock它缺少了(通過synchronized塊或者方法所提供的)隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖

等多種synchronized關鍵字所不具備的同步特性,何以見得,舉個簡單的例項:

假設我們需要先獲得鎖A,然後在獲取鎖B,當鎖B獲得後,釋放鎖A同時獲取鎖C,當鎖C獲得後,在釋放B同時獲得鎖D。。。是不是已經被繞暈了,很顯然如果使用synchronized實現的話,不但其過程複雜難以控制,並且稍微出錯可以說是一種災難性的後果。

這裡寫圖片描述

而關於Lock介面的使用,也在上一篇的內容中詳細的介紹了關係Lock介面的使用案例。下邊幾張圖顯示了Lock相關類在Java 8 concurrent併發包下的大致位置和關係。

1、Java 8中locks包下的類:

這裡寫圖片描述

2、他們之間大致的繼承和實現關係如下:

這裡寫圖片描述

從上述截圖中可以看到Lock介面的實現主要有:ReentrantLock,其中ReentrantLock中使用到了AbstractQueuedSynchronizer(佇列同步器),下邊會一起探討一下AbstractQueuedSynchronizer的設計與實現。

3、Lock介面的定義:

這裡寫圖片描述

4、Lock各介面的含義:

這裡寫圖片描述

Lock介面定義了實現一個鎖應該具有的方法,下邊看一下AQS。

二、佇列同步器AQS

佇列同步器(簡稱:同步器)AbstractQueuedSynchronizer(英文簡稱:AQS,也是面試官常問的什麼是AQS的AQS),是用來構建鎖

或者其他同步元件的基礎框架,它使用了一個int成員變量表示同步狀態,通過內建的FIFO佇列來完成資源獲取執行緒的排隊工作。

這裡暴露出了兩個含義:

(1)第一個就是我們知道如果我們使用鎖同步共享變數的時候,我們首先應該要知道這個共享變數的狀態(是否已經被其他執行緒鎖住等),這也是這個int成員變數的作用;

(2)第二個就是既然是同步訪問共享資源,肯定會有一些執行緒無法獲取到共享資源等待獲取鎖而進入一個容器中進行儲存而這容器就是這個內建的FIFO佇列。

同步器的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法(這裡說抽象方法並不準確,因為他雖然是一個抽象類,但是並沒有abstract修飾的抽象方法)來管理同步狀態,在抽象方法的實現過程中免不了要對**同步狀態(上文中說的int成員變數)**進行更改,這時就需要使用同步器提供的3個方法(getState()、setState()、compareAndSetState())來進行操作,因為它們能夠保證狀態的改變是安全的。

子類推薦被定義為自定義同步元件的靜態內部類,同步器自身沒有實現任何同步介面,它僅僅是定義了若干同步狀態獲取和釋放的方法來供自定義同步元件使用,同步器既可以支援獨佔式地獲取同步狀態,也可以支援共享式地獲取同步狀態,這樣就可以方便實現不同型別的同步組(ReentrantLock、ReentrantReadWriteLock、CountDownLatch等)。

同步器是實現鎖(也可以是任意同步元件)的關鍵,在鎖的實現中聚合同步器,利用同步器實現鎖的語義。可以這樣理解二者之間的關係:

(1)鎖是面向使用者的,它定義了使用者與鎖互動的介面(比如可以允許兩個執行緒並行訪問),隱藏了實現細節;

(2)** 同步器面向的是鎖的實現者**,它簡化了鎖的實現方式,遮蔽了同步狀態管理、執行緒的排隊、等待與喚醒等底層操作。或者可以把AQS認為是鎖的實現者的一個父類。

鎖和同步器很好地隔離了使用者實現者所需關注的領域。

1、首先看一下AbstractQueuedSynchronizer的主要方法:

在看具體的AbstractQueuedSynchronizer方法之前,我們可以大致將AbstractQueuedSynchronizer的方法分為如下幾種:

(1)public final
(2)protected final
(3)private
(4)protected

  
  • 1
  • 2
  • 3
  • 4

AbstractQueuedSynchronizer是一個抽象類,但是卻沒有一個抽象方法,但是主要的方法可以分為上述的四種,我們知道final修飾的方式是不可以被子類重寫的,protected修飾的方法是可以被子類過載的,下邊展示一下大致分的四類方法。

(1)protected類別

這裡寫圖片描述

具體程式碼如下:

protected boolean tryAcquire(int arg) {
	throw new UnsupportedOperationException();
}

protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
//另外幾個也是直接丟擲異常!

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

AbstractQueuedSynchronizer雖然沒有抽象方法,但是提供了五個方法可以讓我們在子類中過載,並且這五個方法都是空實現直接丟擲異常,也就是說我們要使用這五個方法提供的功能,我們必須要自己在子類中進行實現,這也是“模板方法模式”的一種體現和使用。這五個方法的具體含義如下:

這裡寫圖片描述

上述五個方法稱之為:同步器可重寫的方法,究其原因,可以根據上述分為四個種類的方法修飾符進行理解。

(2)public final類別

除了上述protected類別的方法,還有一個關鍵的類別就是public final類別,這是因為,這是我們可以直接使用的方法,稱之為“模板方法”,當我們實現自定義的同步元件的時候,我們可以呼叫這些模板方法獲取我們需要的東西。主要有如下方法:

這裡寫圖片描述

常用的模板方法方法含義如下:

這裡寫圖片描述

同步器提供的上述模板方法基本上分為3類:獨佔式獲取與釋放同步狀態共享式獲取與釋放同步狀態查詢同步佇列中的等待執行緒情況

自定義同步元件將使用同步器提供的模板方法來實現自己的同步語義。只有掌握了同步器的工作原理才能更加深入地理解併發包中其他的併發元件。

(3)protected final類別

上文中,我們至少應該知道了我們要對int型別的同步狀態進行修改,下邊的三個方法提供了可以修改:

這裡寫圖片描述

另外還有三個:hasWaiters、getWaitQueueLength、getWaitingThreads三個方法。

2、再看一下AbstractQueuedSynchronizer的內部類:

這裡寫圖片描述

從上圖中可以看到AbstractQueuedSynchronizer有兩個內部類:一個是ConditionObject,另一個是Node。

3、ConditionObject內部類:

(1)ConditionObject

這個我們知道在使用synchronized的時候是使用wait和notify進行執行緒間通訊,使用ReentrantLock的時候是使用Condition實現的執行緒間通訊,而這正是AbstractQueuedSynchronizer幫我們進一步封裝的Condition介面:

(2)Condition介面如下:

這裡寫圖片描述

(3)ConditionObject實現了Condition介面:

這裡寫圖片描述

(4)呼叫ReentrantLock的newCondition方法正是返回的ConditionObject物件:

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

//這個newCondition是Sync裡的一個方法
final ConditionObject newCondition() {
return new ConditionObject();
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

4、Node內部類:

這裡寫圖片描述

同步器依賴內部的同步佇列(一個FIFO雙向佇列)來完成同步狀態的管理,當前執行緒獲取同步狀態失敗時,同步器會將當前執行緒以及等待狀態等資訊構造成為一個節點(Node)並將其加入同步佇列,同時會阻塞當前執行緒,當同步狀態釋放時,會把首節點中的執行緒喚醒,使其再次嘗試獲取同步狀態。

(1)同步佇列的基本結構

同步佇列中的節點(Node)用來儲存獲取同步狀態失敗的執行緒引用、等待狀態以及前驅和後繼節點,節點的屬性型別與名稱以及描述如下:

這裡寫圖片描述

節點是構成同步佇列(等待佇列)的基礎,同步器擁有首節點(head)和尾節點(tail),沒有成功獲取同步狀態的執行緒將會成為節點加入該佇列的尾部,同步佇列的基本結構如下圖:

這裡寫圖片描述

(2)由於同一時刻只有一個執行緒能夠獲取到同步鎖,但可以有多個執行緒進入阻塞,也就是說將需要等待的執行緒Node插入到尾部是需要進行同步操作的,使用的方法是:compareAndSetTail(Node expect, Node update) ,只有設定成功後,當前節點才正式與之前的節點建立關聯。

關於Node節點的細節還有很多,最重要的是我們理解他就是實現的是佇列同步的儲存功能就行,這個儲存功能在尾部存放的是需要排隊等待的執行緒,在頭部獲取的是獲取到鎖的執行緒資訊,其他的內容不再進行學習,有興趣的可以參考其他文章或書籍研究。

5、同步狀態:

這裡寫圖片描述

同步狀態被設計為是AQS中的一個整形變數,用於表示當前共享資源的鎖被執行緒獲取的次數,並且是多執行緒可見的。

(1)如果是獨佔式的話state的值0表示該共享資源沒有被其他執行緒所鎖住可以被使用,其他值表示該鎖被當前執行緒重入的次數;例如下文中的重入鎖ReentrantLock

(2)如果是共享式,該 state值被分為高16位和低16位,高16位表示讀狀態,低16位表示寫狀態,用一個整形維護多種狀態。例如:ReentrantReadWriteLock實現讀寫鎖,用整數state表示讀寫鎖狀態,關於ReentrantReadWriteLock後期會介紹。

三、ReentrantLock的設計與實現

ReentrantLock的類圖結構如下:

這裡寫圖片描述

可以看出ReentrantLock的內部類包含:Sync、NonfairSync(非公平鎖)、FairSync(公平鎖)。而Sync正是繼承了AbstractQueuedSynchronizer這個抽象類,而NonfairSyncFairSync又是繼承了Sync的兩個靜態內部類。

因為我們在上述的學習中已經知道了AbstractQueuedSynchronizer同步器面向的是鎖的實現者,即其內部已經封裝了一些關於鎖的操作。這也是上文中提到的兩句話:(1)同步器的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態;(2)子類推薦被定義為自定義同步元件的靜態內部類,同步器自身沒有實現任何同步介面,它僅僅是定義了若干同步狀態獲取和釋放的方法來供自定義同步元件使用。

1、Sync內部類

在這裡Sync是AQS的子類,這個時候我們應該想到上述提到的使用protected 修飾的5個方法,這也是Sync這個子類需要重寫的,Sync內部類圖如下:

這裡寫圖片描述

tryRelease()方法的作用已經在上邊解釋了,這裡不再贅述!

可以看出對於我們上述說的那5個方法,Sync只重寫了一個:tryRelease(),那麼其他的幾個方法那?

這裡需要注意的是:Sync也是一個abstract類,並且這5個方法並不是一定要在子類中進行重寫的,ReentrantLock的幾個內部類只重寫了tryReleasetryAcquire方法,其他的使用是在ReentrantReadWriteLock中用到的,這也是根據具體的ReentrantLock的實現的實際需求,而其他的方法具體(其實在ReentrantLock就是指tryAcquire)的重寫這就需要:NonfairSyncFairSync上場了!

2、NonfairSync和FairSync內部類

NonfairSync和FairSync實現差不多,這裡只學習FairSync。

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
    final void lock() {
        acquire(1);
    }

    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    protected final boolean tryAcquire(int acquires) {
        //省去具體實現
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

FairSync 實現了Sync的抽象方法lock(),而具體的tryAcquire()方法即是重寫AQS中的tryAcquire()方法,這裡的lock() 方法呼叫了AQS提供的acquire() 方法,AQS中acquire方法如下:

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

  
  • 1
  • 2
  • 3
  • 4

AQS中的acquire()方法呼叫了AQS中的tryAcquire()方法,但tryAcquire()上述說的他是一個空實現,直接丟擲的異常,而最終是由FairSync 重寫了,所以此時執行的時候,真正呼叫的就是FairSync 中的tryAcquire()方法。而我們在使用ReentrantLock的lock或者unlock方法的時候,實際上呼叫的就是ReentrantLock實現的Lock的介面,而這個介面的實現內部又是呼叫的Sync裡的抽象方法lock()

3、獨佔式和共享式

前邊介紹到的AQS支援獨佔式獲取與釋放同步狀態、共享式獲取與釋放同步狀態。而ReentrantLock被設計為獨佔式的獲取與釋放同步狀態,意思就是排他鎖,即同一時刻只能有一個執行緒獲取到鎖,這裡的同步狀態是AQS的一個全域性變數state

至此,整個的結構大致理了一遍,雖然還有很多細節沒有探討過。

如果,我們對上述的繼承關係什麼的還不是很懂的話,以及對AQS是如何實現鎖的還不瞭解的話,我們倒不如使用AQS自己設計一個鎖,類似ReentrantLock,或者說是ReentrantLock的精簡版。

四、使用AQS自己實現一個鎖

在上邊的學習中,我們知道要是實現一個自定義的Lock實現類,首先要實現Lock介面,並且定義一個內部類繼承AQS類,重寫他的方法,示例如下:

public class SimplifyReentrantLock implements Lock {
private final Sync sync = new Sync();

/**
 * AQS的子類Sync
 */
private static class Sync extends AbstractQueuedSynchronizer {

    @Override
    protected boolean isHeldExclusively() {
        //是否處於佔用狀態
        return getState() == 1;
    }

    @Override
    protected boolean tryAcquire(int arg) {
        //當狀態為0是獲取鎖
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }

    @Override
    protected boolean tryRelease(int arg) {
        //釋放鎖,將狀態設定為0
        if (getState() == 0) {
            throw new IllegalMonitorStateException();
        }
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }

    Condition newCondition() {
        return new ConditionObject();
    }

}

@Override
public void lock() { sync.acquire(1); }

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

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

@Override
public boolean tryLock() { return sync.tryAcquire(1); }

@Override
public void lockInterruptibly() throws InterruptedException { }

@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
    return false;
}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63

測試用例:

public class SimplifyReentrantLockDemo {
public static void main(String[] args) {
    SimplifyReentrantLock lock = new SimplifyReentrantLock();
    Condition condition = lock.newCondition();

    new Thread(() -> {
        lock.lock();
        try {
            System.out.println("進入等待!");
            condition.await();
            System.out.println("接收到通知!繼續執行!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock.unlock();
    }, "conditionAwaitThread").start();

    new Thread(() -> {
        try {
            System.out.println("模擬3秒後傳送通知過!");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock.lock();
        System.out.println("傳送通知!");
        condition.signal();
        lock.unlock();
    }, "conditionSignalThread").start();
}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

執行結果:

這裡寫圖片描述

上述程式碼,一個重要的卻別就是沒有ReentrantLock中的NonfairSync和FairSync,那麼假設我們新增一個公平鎖的話,想起來還是很簡答的,直接參考ReentrantLock即可,這裡不再贅述。


參考文章:

1、http://blog.csdn.net/pfnie/article/details/53191892

2、部分截圖和內容參考自《Java併發程式設計的藝術》

3、http://ifeve.com/introduce-abstractqueuedsynchronizer/

        </div>