1. 程式人生 > 其它 >【Java學習筆記(九十三)】之atomic包,AQS,鎖,讀寫鎖,Condition

【Java學習筆記(九十三)】之atomic包,AQS,鎖,讀寫鎖,Condition

技術標籤:Java學習筆記# 多執行緒佇列多執行緒java併發程式設計

本文章由公號【開發小鴿】釋出!歡迎關注!!!


老規矩–妹妹鎮樓:

一. atomic包

(一) 概述

JUC中有一個包java.util.concurrent.atomic中存放著原子操作的類,如AtomicInteger,大致保證闊基本型別,引用型別,陣列型別,物件的屬性修改器型別,JDK1.8新增類。

(二) 基本型別

使用原子的方式更新基本型別,如AtomicInteger,AtomicLong的主要API如下所示:

get() 返回值
getAndAdd(int) 增加指定的資料,返回變化前的資料
getAndDecrement
() 減少1,返回減少前的資料 getAndIncrement() 增加1,返回增加前的資料 getAndSet(int) 設定指定的資料,返回設定前的資料 addAndGet(int) 增加指定的資料後返回增加後的資料 decrementAndGet() 減少1,返回減少後的值 incrementAndGet() 增加1,返回增加後的值 lazySet(int) 僅當get時才會set compareAndSet(int,int) 嘗試新增後對比,如果增加成功則返回true

AtomicBoolean類的主要API如下所示:

compareAndSet(Boolean, Boolean)
引數為原始值和修改的新值,修改成功返回true getAndSet(Boolean) 嘗試設定新的boolean值,返回設定前的值

(三) 引用型別

AtomicStampedReference類僅僅是AtomicReference類的再一次封裝,增加了一層引用和計數器,計數器的設定是由自己控制的,可以按照自己的方式標識版本號,一般是自增操作。

AtomicMarkableReference和AtomicStampedReference功能差不多,只不過描述的只是兩種狀態,是與否,而AtomicStampedReference是多種狀態。


(四) 陣列型別

使用原子的方式更新數組裡的某個元素,如AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray。很多API用法與基本型別都是相似的,不在贅述。


(五) 物件的屬性修改器型別

如果需要原子更新某個類中的某個欄位時,需要用到物件的屬性修改器型別原子類。如AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater。但是修改的物件是有一些限制的,如下所示:

1. 操作的目標不能是static型別,因為CAS使用的Unsafe類的方法提取的都是非static型別的屬性偏移量,如果目標是static型別,在獲取時沒有使用對應的static方法會報錯的。

2. 操作的目標不能是final型別,因為final型別無法修改。

3. 必須是volatile型別,即記憶體可見的。

4. 屬性必須對當前的修改器updater所在的區域是可見的,如果是private則必須是當前類中;如果是protected則必須有父子關係;如果是default則必須在同一個包下。


(六) JDK1.8新增類

雖然普通的atomic類方法已經通過CAS優化了原子操作,但是對於高併發的場景,多個執行緒CAS都會失敗,並陷入無限的自旋鎖中,浪費資源。因此,JDK1.8後,新增了多個類來優化這種缺陷。原有的atomic類由於多個執行緒同時競爭一個變數而造成CAS失敗,因此,如果將該變數分解為多個變數,讓每個執行緒都能夠獲取到變數進行CAS操作不就可行了嗎!是的,新增的類就是這種思路,有LongAdder, DoubleAdder, LongAccumulator, DoubleAccumulator,這些新增的類的效能對於多執行緒的情況優勢十分明顯。


二. AQS

(一) 概述

AQS(Abastract Queue Synchrinizer),佇列同步器。它是構建鎖或者其他同步元件的基礎框架(如ReentranLock, ReentrantReadWriteLock, Semaphore等),是JJUC併發包中的核心基礎元件。

(二) 優勢

AQS解決了實現同步器時涉及到的大量細節問題,如獲取同步狀態,FIFO同步佇列。基於AQS來構建同步器可以極大地減少工作,也不必處理在多個位置的競爭問題。

(三) state同步狀態

AQS維護了一個volatile int型別的變數state表示當前的同步狀態,當state>0時表示已經獲取了鎖,當state=0表示釋放了鎖。有以下三個方法操作state:

getState() //返回state值

setState() //設定當前同步狀態

compareAndSetState() //使用CAS設定當前狀態,保證狀態設定的原子性,依賴於Unsafe類的compareAndSwapInt()方法實現


(四) 資源共享方式

AQS定了了兩種資源共享方式:

  1. Exclusive,獨佔方式,只有一個執行緒能夠執行,如ReentrantLock

  2. Share, 共享方式, 多個執行緒可以同時執行,如Semaphore / CountDownLatch

(五) CHL同步佇列

AQS內部維護著一個CHL同步佇列,遵循FIFO原則,AQS依賴它來完成同步狀態的管理。每個執行緒排隊進入CHL佇列中,當前執行緒如果獲取同步狀態失敗時,AQS會將當前執行緒已經等待的狀態資訊構造成一個節點,加入到CHL同步佇列中,同時會阻塞當前執行緒,當同步狀態釋放時,會把隊頭的節點去掉,喚醒後面一個節點,使其再次嘗試獲取同步狀態。佇列能夠保證每個時刻只有一個執行緒能夠獲取到同步狀態,因此出佇列的操作不需要使用CAS來保證原子性,而不同的執行緒入佇列的操作需要保證原子性,因為同時會有不同的執行緒入佇列。


三. 鎖

(一) 鎖的型別

1. 互斥鎖

物件互斥保證共享資料操作的原子性,每個物件都對應一個可稱為“互斥鎖”的標記,用來保證在任一時刻,只能有一個執行緒訪問該物件。

2. 阻塞鎖

讓執行緒進入阻塞狀態進行等待,當獲得相應的訊號(喚醒,時間)時,才可以進入執行緒的準備就緒狀態,就緒狀態的所有執行緒通過競爭獲得物件鎖。

3. 自旋鎖

讓當前執行緒不停地執行無意義的迴圈,當迴圈的條件被其他執行緒改變時,才能夠進入臨界區。這種鎖由於不進行執行緒狀態的改變,因此響應速度更快,但是如果多執行緒競爭著鎖,就會出現持續自旋的資源浪費。


4. 讀寫鎖

特殊的自旋鎖,將共享資源的訪問者分為了讀者和寫著,讀者只對共享資源進行讀訪問,寫者需要對共享資源進行寫操作。讀操作可以併發執行,而寫操作是排他的,只能單執行緒進行,不能同時有讀者和寫者。

5. 公平鎖

公平鎖加鎖前檢查是否有排隊等待的執行緒,優先排隊等待的執行緒,先來先得。非公平鎖在加鎖前不考慮排隊等待問題,直接嘗試獲取鎖,獲取不到就到隊尾等待。非公平鎖的效能搞好,因為公平鎖需要在多核的情況下維護一個佇列。


(二) ReentrantLock

1. 概述

可重入鎖,是一種遞迴無阻塞的同步機制,即可以在鎖中巢狀另一個鎖,通過同步狀態值來判斷當前鎖的狀態。該鎖等同於synchronized的使用,但是ReentrantLock更加靈活,強大,能夠減少死鎖的發生機率。

2. 構造方法

通過在構造方法中傳入boolean引數,表示是否是公平鎖,預設是非公平鎖,傳入true表示是公平鎖。

構造方法原始碼如下所示:

public ReentrantLock() {
    //非公平鎖
    sync = new NonfairSync();
}

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

Sync是ReentrantLock裡的一個內部類,它繼承了AQS,有兩個子類,一個是公平鎖FariSync,另一個是非公平鎖NonfairSync。

(三) 獲取鎖

建立鎖物件,通過lock方法來獲取鎖。

ReentrantLock lock = new ReentrantLock();
lock.lock();

lock方法也是呼叫的Sync類的lock方法:
public void lock(){
	sync.lock();
}

最後會呼叫AQS同步佇列的方法來加鎖。

(四) 釋放鎖

獲取同步鎖,使用完畢後需要釋放鎖,呼叫unlock()方法,該方法中呼叫了Sync類的release()方法,release方法定義在AQS中,原始碼如下:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

其中使用的tryRelease()方法的原始碼如下所示:

protected final boolean tryRelease(int releases) {
    //減掉releases
    int c = getState() - releases;
    //如果釋放的不是持有鎖的執行緒,丟擲異常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    //state == 0 表示已經釋放完全了,其他執行緒可以獲取同步狀態了
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

可以看到,由於ReentrantLock鎖是可重入鎖,可以巢狀鎖,因此同步狀態可能有好幾層,只有每一層巢狀的鎖都釋放掉後,同步佇列的狀態state=0,則將鎖持有的執行緒釋放。

(五) 公平鎖和非公平鎖

公平鎖在獲取同步狀態時多了一個限制條件,判斷當前的執行緒是否位於CHL同步佇列中的第一個,如果是則可以獲取鎖,這就保證了FIFO的公平性。

(六) ReentrantLock對於Synchronized的優勢

1. 提供更多的功能,如時間鎖等候,可中斷鎖等候,鎖投票。

2. 提供了Condition,能夠更加靈活地操作執行緒的等待,喚醒,比如可以通過Condition和ReentrantLock的繫結,僅僅喚醒部分的執行緒。

3. 提供了可輪詢的鎖請求,嘗試地去獲取鎖,如果成功則繼續,否則可以等到下次執行時處理,而不像synchronized那樣對於鎖請求只有成功和阻塞兩種結果,這樣ReentrantLock不容易產生死鎖。

4. 更加靈活的同步程式碼塊,使用synchronized時,只能在同一個synchronized塊結構中獲取和釋放,注意,ReentrantLock的鎖釋放一定要在finally中。

5. 支援中斷處理,效能更好。


四. 讀寫鎖ReentrantReadWriteLock

(一) 概述

大多數場景下,大部分時間提供的都是讀服務,寫服務的時間很少,因此如果使用ReentrantLock這個互斥鎖,會對讀執行緒的效能造成很大的影響,讀寫鎖由此而生。它維護著一對鎖,一個讀鎖和一個寫鎖,允許多個讀執行緒併發,只允許寫執行緒單執行緒執行,且寫執行緒執行時,所有的讀和寫執行緒都被阻塞。

(二) 特徵

1. 公平性

支援公平鎖和非公平鎖。

2. 重入性

支援重入,讀和寫鎖都可以巢狀65535個鎖。

3. 鎖降級

寫鎖能夠降級稱為讀鎖,讀鎖不能升級為寫鎖。

(三) 實現

ReentrantReadWriteLock實現了介面ReadWriteLock,該介面維護了一對相關的鎖,一個用於只讀操作,另一個用於寫入操作:

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

ReadWriteLock類定義了兩個方法,readLock()返回讀鎖,writeLock()返回寫鎖,最終的鎖主體還是要依靠Sync來實現的。所以ReentrantReadWriteLock實際上只有一個鎖,只是在獲取讀鎖和寫鎖的方式上不一樣而已,都是由Lock類實現。

ReentrantLock中使用state整數來表示同步狀態,該值表示鎖被一個執行緒重複獲取的次數,但是讀寫鎖ReentrantReadWriteLock內部維護著一對鎖,需要用一個變數來維護多種狀態。所以讀寫鎖採用按位切割使用的方式維護這個變數,將其切分為兩部分,高16位表示讀,低16位表示寫。


(四) 寫鎖的獲取

寫鎖是一個支援可重入的互斥鎖,獲取寫鎖最終會呼叫Sync類中的tryAcquire(int arg)方法,該方法和ReentrantLock中的tryAcquire(int arg)方法大致一樣,不過在判斷重入的時候添加了一個條件:是否存在讀鎖。因為要 確保寫鎖的操作對讀鎖是可見的,不能在讀鎖存在的情況下獲取寫鎖,這樣會導致寫鎖操作的不可見。只有等待所有讀鎖釋放完畢後,寫鎖才能夠被當前執行緒獲取。

(五) 寫鎖的釋放

寫鎖釋放鎖的整個過程和ReentrantLock相似,由於存在鎖的巢狀操作,每次釋放都是減少寫狀態值,當寫狀態值為0時表示寫鎖已經完全釋放了,從而其他等待的執行緒可以繼續訪問讀寫鎖,獲取同步狀態。


(六) 讀鎖的獲取

讀鎖是一個可重入的共享鎖,它能夠被多個執行緒共同持有,在沒有其他寫執行緒的訪問時,讀鎖總是獲取成功的。

(七) 讀鎖的釋放

unlock()方法釋放讀鎖。

(八) 鎖降級

由於寫鎖的優先順序是高於讀鎖的,因此鎖降級只能由寫鎖降級為讀鎖,讀鎖無法升級為寫鎖。鎖降級遵循以下的順序:首先獲取寫鎖,然後獲取讀鎖,最後釋放掉寫鎖,就只剩下讀鎖了,降級成功。


五. Condition

(一) 概述

Condition對執行緒的等待,喚醒操作更加靈活,原來通過synchronized的wait()和notify()方法實現的等待通知模式要麼只能喚醒一個執行緒或者所有執行緒。Condition能夠靈活地喚醒指定的部分執行緒。

Condition必須配合鎖一起使用,因為對共享狀態變數的訪問發生在多執行緒環境中,一個Condition的例項必須和一個Lock繫結,因此Condition一般都是作為Lock的內部實現。

(二) Condition的實現

1. AQS的內部類

獲取一個Condition必須通過Lock的newCondition()方法,該方法定義在介面Lock下,返回的結果是繫結到此Lock例項的Condition例項。Condition是一個介面,其下僅有一個實現類ConditionObject,由於Condition的操作需要獲取相關的鎖,而AQS是同步鎖的實現基礎,所以ConditionObject定義為AQS的內部類,定義如下:

public class ConditionObject implements Condition, java.io.Serializable {
}

2. 等待佇列

每個Condition物件都包含著一個FIFO佇列,佇列中每個節點都包含著一個執行緒引用,該執行緒就是在Condition物件上等待的執行緒,當前執行緒呼叫await()方法時,將會將當前執行緒構造成一個節點放入佇列的尾部。過程與AQS中的CHL同步佇列差不多,使用的都是同一個類(AbstractQueuedSynchronized.Node靜態內部類)。

3. 等待狀態

呼叫Condition的await()方法會使當前執行緒進入等待狀態,同時加入到Condition等待佇列中並且釋放鎖。然後不斷檢測該節點代表的執行緒是否出現在CLH同步佇列中,如果存在則存在說明該執行緒被喚醒了,參與鎖的競爭;如果不存在,則說明還在等待,則繼續掛起。當從await()方法返回時,當前執行緒一定是獲取了Condition相關的鎖。

4. 通知

呼叫Condition的signal()方法,首先判斷當前執行緒是否已經獲得了鎖,只有沒有鎖的執行緒才能夠喚醒。然後喚醒在等待佇列中的頭結點,完成佇列的頭結點修改工作,並且將舊的頭結點移動到CHL同步佇列中,參與鎖的競爭。