Java進階知識點7:不要只會寫synchronized - JDK十大併發編程組件總結

1、背景

提到Java中的併發編程,首先想到的即是使用synchronized代碼塊,保證代碼塊在併發環境下有序執行,從而避免衝突。若是涉及多線程間通訊,能夠再在synchronized代碼塊中使用wait和notify進行事件的通知。java

不過使用synchronized+wait+notify進行多線程協做編程時,思惟方式過於底層,經常須要結合具體的併發場景編寫大量額外的控制邏輯。編程

好在java.util.concurrent包下已經爲咱們準備好了大量適用於各種併發編程場景的組件,利用這些組件咱們能夠快速完成複雜的多線程協做編程,並且這些組件都是通過高度性能優化的。安全

2、經常使用的併發編程組件

2.1 ReentrantLock

synchronized代碼塊本質上完成的是代碼片斷的自動上鎖和解鎖,以確保關鍵代碼片斷在多線程中的互斥訪問。性能優化

因此,提及synchronized的替代品,首先想到的即是Lock,而ReentrantLock(可重入鎖)即是Lock中最經常使用的組件。之因此稱之爲可重入鎖,是由於同一個線程能夠對同一個ReentrantLock對象屢次嵌套加鎖,只要按照加鎖流程,依次完成相同次數的解鎖就不會有問題。多線程

相比於synchronized代碼塊,ReentrantLock最大的特色就是靈活。一般咱們選用ReentrantLock而不是synchronized代碼塊,會出於以下三種考慮:併發

一、須要更加靈活的加鎖和解鎖時機控制ide

好比以下模擬的鎖耦合場景(釋放當前鎖以前必須獲取另一個鎖),用synchronized是很難完成的,由於你永遠沒法讓兩個synchronized代碼塊交叉重疊。函數

ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();
lockA.lock();
lockB.lock();
lockA.unlock();
lockB.unlock();

二、須要支持非阻塞的鎖機制高併發

 synchronized代碼片斷一旦出現多線程資源爭用,代碼會一直卡住,直到其餘線程釋放資源(代碼執行出了synchronized片斷)後,本線程搶得資源的使用權爲止。性能

極端狀況下,可能會出現本線程一直沒法獲取資源使用權的狀況,而這種狀況下,synchronized關鍵字沒有任何後續補救措施。

而ReentrantLock在這一點上要人性化不少,它不光提供了tryLock()這類非阻塞的嘗試獲取鎖的方法,也提供了tryLock(long timeout, TimeUnit unit)這種帶超時機制的嘗試獲取鎖的方法。即使是最壞的死鎖狀況發生,至少你的程序可以在必定時長的等待後,打破死鎖,進而得到報警/恢復/忽略繼續執行等後續邏輯處理的可能性。

三、想要得到更高的性能

在JDK6以前,synchronized的性能與ReentrantLock相比仍是有較大差距,特別是高併發場景下,synchronized的性能可能會急劇衰減。因此那時會經過使用ReentrantLock替換synchronized代碼塊進行特定場景下的性能優化。

不過JDK7中已經對synchronized作了優化,性能與ReentrantLock已經很接近了,而JDK8中連ConcurrentHashMap的實現都開始用synchronized替換以前版本的ReentrantLock。Java官方也推薦你們儘可能使用synchronized關鍵字,畢竟用它編寫的代碼要顯得更加優雅,也不會發生忘記解鎖的狀況。至於synchronized的性能,如今不光不會拖後腿,您使用它還將享受Java對其堅持不懈的優化。

2.2 ReadWriteLock

ReadWriteLock本質是一對相互關聯的ReadLock和WriteLock,ReadLock和WriteLock各自的使用方式與2.1中介紹的ReentrantLock一致,不過ReadLock和WriteLock間有必定的制約關係。

不一樣線程能夠同時對同一個ReadWriteLock中的ReadLock加鎖(即調用readWriteLock.readLock().lock()),但只要想對ReadWriteLock中的WriteLock加鎖(即調用readWriteLock.writeLock().lock()),則必須等待其餘線程中已經持有的ReadLock和WriteLock的都解鎖後才能成功。

在共享資源讀多寫少,且多線程同時讀共享資源不會有問題的場景下,一般只要作到多線程間讀與讀能夠共存,讀與寫不能共存,寫與寫不能共存,便可保證共享資源的併發訪問不會有問題。這即是ReadWriteLock的典型適用場景。

2.3 Semaphore

Semaphore,又叫信號量,是專門用於只容許N個任務同時訪問某個共享資源的場景。

它使用很簡單,典型使用方式以下所示:

Semaphore semaphore = new Semaphore(10); //建立信號量,併爲該信號量指定初始許可數量
semaphore.acquire(); //獲取單個許可,若是無可用許可前將一直阻塞等待
semaphore.acquire(2); //獲取指定數目的許可,若是無可用許可前將會一直阻塞等待
semaphore.tryAcquire(); //嘗試獲取單個許可,若是無可用許可直接返回false,不會阻塞
semaphore.tryAcquire(2); //嘗試獲取指定數量的許可,若是無可用許可直接返回false,不會阻塞
semaphore.tryAcquire(2, 2, TimeUnit.SECONDS); //在指定的時間內嘗試獲取指定數量的許可,若是在指定的時間內獲取成功,返回true,不然返回false。
semaphore.release(); //釋放單個許可
semaphore.release(2); //釋放指定數量的許可
int availablePermits = semaphore.availablePermits(); //查詢信號量當前可用許可數量

須要特別注意的是,信號量並無嚴格要求釋放的許可數量必須與已申請的許可數量一致。也就是說屢次調用release方法,會使信號量的許可數增長,達到動態擴展許可的效果。例如:初始permits 爲1,調用了兩次release(),availablePermits會改變爲2,這在應對某些須要動態提高併發任務量的需求時特別有用。

2.4 AtomicInteger

AtomicInteger、AtomicLong、AtomicDouble等都是對基本類型的Atomic封裝,爲基本類型封裝線程安全的訪問方式。特別適用於須要多線程同時操做一個數值變量,完成累積計數統計等操做的場景。

它們的底層實現原理都基於以下兩點:

一、使用volatile關鍵字保證多線程間對數值更新的實時可見性。

二、使用CAS操做保證操做的原子性。CAS操做會在對值進行更新前,檢查該值是不是以前讀取到的舊值,若是是,則說明該值目前尚未其餘線程更新,不存在併發衝突,能夠安全設置爲新值,整個檢查+設置新值的操做一般由CPU提供單指令完成,保證原子性。顯然,CAS可能會失敗(遇到併發衝突時),則自動從新讀取當前值,不斷循環,直至CAS成功。

JDK1.8中爲AtomicInteger增長值源碼以下:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2); //讀取舊值
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); //利用CAS操做將舊值設置爲增長後的新值

    return var5;
}

 AtomicInteger典型操做以下:

AtomicInteger atomicInteger = new AtomicInteger(1);
atomicInteger.compareAndSet(1, 2); //CAS操做,若是當前值等於指望值,則將當前值設置爲新值,並返回true表示設置成功,不然返回false表示設置失敗。該操做是原子性的。

atomicInteger.getAndIncrement(); //當前值加1,同時返回加1前的舊值
atomicInteger.incrementAndGet(); //當前值加1,同時返回加1後的新值

atomicInteger.getAndDecrement(); //當前值減1,同時返回減1前的舊值
atomicInteger.decrementAndGet(); //當前值減1,同時返回減1後的新值

atomicInteger.getAndAdd(3); //當前值加3,同時返回加3前的舊值
atomicInteger.addAndGet(3); //當前值加3,同時返回加3後的新值

 因爲Atomic採用無鎖化設計,在高併發場景下一般擁有較好的性能表現。

2.5 CountDownLatch

 CountDownLatch能夠設置一個初始計數,一個線程能夠調用await等待計數歸零。其餘線程能夠調用countDown來減少計數。

計數不可被重置,CountDownLatch被設計爲只觸發一次。

CountDownLatch的典型操做以下:

CountDownLatch countDownLatch = new CountDownLatch(5); //初始化CountDownLatch並設置初始計數值
countDownLatch.countDown(); //將計數值-1
countDownLatch.await(); //等待直至計數值爲0
countDownLatch.await(2, TimeUnit.MINUTES); //等待直至計數值爲0,或者超時時間達到

咱們經常會將一個任務拆分爲可獨立運行的N個任務,待N個任務都完成後,再繼續執行後續任務。這即是CountDownLatch的典型應用場景。

2.6 CyclicBarrier

 CyclicBarrier 的字面意思是可循環使用(Cyclic)的屏障(Barrier)。它要作的事情是,讓一組線程到達一個屏障(也能夠叫同步點)時被阻塞,直到最後一個線程到達屏障時,屏障纔會打開,屆時全部被屏障攔截的線程同時開始繼續執行。

CyclicBarrier默認的構造方法是CyclicBarrier(int parties),其參數表示屏障攔截的線程數量。每一個線程經過調用CyclicBarrier的await方法告訴CyclicBarrier我已經到達了屏障,而後當前線程被阻塞,直至全部要攔截的線程都調用了CyclicBarrier的await後,你們同時解鎖。

CyclicBarrier還提供另一個構造函數CyclicBarrier(int parties, Runnable barrierAction),用於在屏障打開前,先執行barrierAction,方便處理更復雜的業務場景。

2.7 BlockingQueue

隊列是解決線程間通訊的利器,幾乎絕大部分使用wait、notify這類底層線程間通訊的編程風格均可以重構爲更簡單的隊列模型,線程間的協做問題能夠經過隊列中的消息傳遞來解決。

BlockingQueue(有界阻塞隊列)即是最經常使用的一種隊列。

BlockingQueue的典型操做以下:

BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(100); //初始隊列並設置隊列最大長度爲100
blockingQueue.put("message"); //往隊尾插入新消息,若是隊列已滿將一直等待隊列有可用空間爲止
blockingQueue.offer("message"); //往隊尾插入新消息,若是隊列已滿致使沒法插入,則直接返回false表示插入失敗;若是隊列未滿,能夠成功插入,則返回true表示插入成功
blockingQueue.offer("message", 2, TimeUnit.MINUTES); //普通offer的加強版,能夠指定超時時間,若是沒法插入先嚐試等待指定超時時間,超時時間達到後還沒法插入,直接返回false表示插入失敗;超時時間達到前能夠插入,則成功插入並返回true

String message = blockingQueue.take(); //從隊頭取出最新消息,若是隊列爲空,沒有最新消息,則一直等待直到有最新消息爲止
message = blockingQueue.poll(); //從隊頭取出最新消息,若是隊列爲空,沒有最新消息,則直接返回null
message = blockingQueue.poll(2, TimeUnit.MINUTES); //普通poll的加強版,能夠指定超時時間,若是沒有最新消息先嚐試等待指定超時時間。若是超時時間到達前有最新消息,則當即取出最新消息;若是超時時間達到後仍沒有最新消息,則當即返回null

2.8 PriorityBlockingQueue

PriorityBlockingQueue(優先級隊列)做爲阻塞隊列的一種特殊形態,是一個帶優先級排序功能的無界阻塞隊列。

PriorityBlockingQueue的典型操做與BlockingQueue基本一致。除了實現BlockingQueue的基本功能之外,PriorityBlockingQueue額外保證每次從對頭取出的元素老是隊列中優先級最高的元素。

因爲須要比較隊列中元素的優先級,因此加入隊列的元素必須實現Comparable接口,或者在構建時指定實現了Comparator接口的比較器。兩個元素將經過compareTo方法進行比較,小的元素的優先級高。

與BlockingQueue不一樣的是,PriorityBlockingQueue是一個無界隊列,構造PriorityBlockingQueue時能夠指定初始容量,但這並不意味着PriorityBlockingQueue是有界的,它會在隊列滿時自動擴容。因此須要特別注意因爲控制邏輯不嚴謹致使內存溢出的風險。

另外,使用PriorityBlockingQueue的迭代器遍歷隊列時,你會發現隊列元素是亂序的(與插入順序不一樣)。事實上PriorityBlockingQueue只保證依次從隊頭取出元素是按照優先級排序的(參考最小堆的實現);隊列也不保證兩個相同優先級元素的順序,他們可能以任意順序返回。

2.9 DelayQueue

DelayQueue(延時隊列)能夠認爲是PriorityBlockingQueue+元素必須實現Delayed接口的特定組合。

DelayQueue也是一個帶優先級功能的無界阻塞隊列,典型操做也同PriorityBlockingQueue基本一致。只不過它對優先級的定義整合了延遲場景的特定抽象。隊列裏存放實現了Delayed接口的元素。Delayed元素實現了getDelay方法,用於獲取剩餘到期時間,實現了CompareTo方法,用於按照到期時間排序,以便肯定優先級。只有存在到期的元素時,才能從DelayQueue中提取元素,該隊列的頭部是到期最久的元素。

Delayed接口的典型實現方式以下:

public class DelayedElement implements Delayed {
    private final long deadlineMillis;

    public DelayedElement(long deadlineMillis) {
        this.deadlineMillis = deadlineMillis;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        //計算剩餘到期時間,並將剩餘到期時間根據傳入的時間單位進行換算
        return unit.convert(deadlineMillis - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed other) {
        if (other == this) {
            return 0;
        }
        //剩餘到期時間少的,優先級更高
        long diff = (getDelay(TimeUnit.MILLISECONDS) -
                other.getDelay(TimeUnit.MILLISECONDS));
        return (diff == 0) ? 0 : ((diff < 0) ? -1 : 1);
    }
}

DelayQueue特別適用於離散事件仿真。在離散事件仿真場景下,每一個線程模擬一個獨立的實體,在各個特定的時間,向其餘線程的實例或總控模塊發出約定的事件,使用DelayQueue做爲事件消息的傳遞通道,只需根據事件應當發生的時間實現Delayed接口便可,無需每一個線程都引入一個計時器去定時觸發事件的發生。

2.10 Exchanger

Exchanger能夠在兩個線程之間交換數據,當線程A調用Exchanger對象的exchange()方法後,他會陷入阻塞狀態,直到線程B也調用了exchange()方法,而後以線程安全的方式交換數據,以後線程A和B繼續運行。

Exchanger的典型操做以下:

Exchanger<String> exchanger = new Exchanger<>();
String theirMessage = exchanger.exchange("myMessage"); //將本身的消息與對方的消息進行交換,若是對方沒有調用exchange方法,我方將一直等待
theirMessage = exchanger.exchange("myMessage", 2, TimeUnit.MINUTES); //帶超時功能的exchange方法,避免無限等待,若是超時將拋出TimeoutException

Exchanger的使用頻率相對低一些,由於一般涉及多線程編程,都不會恰好只有兩個線程爭用共享資源。在一些簡單的只涉及兩個線程間通訊的雙線程協做場景下,使用Exchanger會讓你的編程更加輕鬆。

3、總結

多線程協做編程,須要注意線程安全問題。使用JDK自帶的併發編程組件,可讓多線程編程更加輕鬆和安全。本文總結了十大JDK中常見的併發編程組件的典型用法和適用場景,但願對你們有所幫助。

相關文章
相關標籤/搜索