提到Java中的併發編程,首先想到的即是使用synchronized代碼塊,保證代碼塊在併發環境下有序執行,從而避免衝突。若是涉及多線程間通訊,能夠再在synchronized代碼塊中使用wait和notify進行事件的通知。java
不過使用synchronized+wait+notify進行多線程協做編程時,思惟方式過於底層,經常須要結合具體的併發場景編寫大量額外的控制邏輯。編程
好在java.util.concurrent包下已經爲咱們準備好了大量適用於各種併發編程場景的組件,利用這些組件咱們能夠快速完成複雜的多線程協做編程,並且這些組件都是通過高度性能優化的。安全
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對其堅持不懈的優化。
ReadWriteLock本質是一對相互關聯的ReadLock和WriteLock,ReadLock和WriteLock各自的使用方式與2.1中介紹的ReentrantLock一致,不過ReadLock和WriteLock間有必定的制約關係。
不一樣線程能夠同時對同一個ReadWriteLock中的ReadLock加鎖(即調用readWriteLock.readLock().lock()),但只要想對ReadWriteLock中的WriteLock加鎖(即調用readWriteLock.writeLock().lock()),則必須等待其餘線程中已經持有的ReadLock和WriteLock的都解鎖後才能成功。
在共享資源讀多寫少,且多線程同時讀共享資源不會有問題的場景下,一般只要作到多線程間讀與讀能夠共存,讀與寫不能共存,寫與寫不能共存,便可保證共享資源的併發訪問不會有問題。這即是ReadWriteLock的典型適用場景。
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,這在應對某些須要動態提高併發任務量的需求時特別有用。
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採用無鎖化設計,在高併發場景下一般擁有較好的性能表現。
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的典型應用場景。
CyclicBarrier 的字面意思是可循環使用(Cyclic)的屏障(Barrier)。它要作的事情是,讓一組線程到達一個屏障(也能夠叫同步點)時被阻塞,直到最後一個線程到達屏障時,屏障纔會打開,屆時全部被屏障攔截的線程同時開始繼續執行。
CyclicBarrier默認的構造方法是CyclicBarrier(int parties),其參數表示屏障攔截的線程數量。每一個線程經過調用CyclicBarrier的await方法告訴CyclicBarrier我已經到達了屏障,而後當前線程被阻塞,直至全部要攔截的線程都調用了CyclicBarrier的await後,你們同時解鎖。
CyclicBarrier還提供另一個構造函數CyclicBarrier(int parties, Runnable barrierAction),用於在屏障打開前,先執行barrierAction,方便處理更復雜的業務場景。
隊列是解決線程間通訊的利器,幾乎絕大部分使用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
PriorityBlockingQueue(優先級隊列)做爲阻塞隊列的一種特殊形態,是一個帶優先級排序功能的無界阻塞隊列。
PriorityBlockingQueue的典型操做與BlockingQueue基本一致。除了實現BlockingQueue的基本功能之外,PriorityBlockingQueue額外保證每次從對頭取出的元素老是隊列中優先級最高的元素。
因爲須要比較隊列中元素的優先級,因此加入隊列的元素必須實現Comparable接口,或者在構建時指定實現了Comparator接口的比較器。兩個元素將經過compareTo方法進行比較,小的元素的優先級高。
與BlockingQueue不一樣的是,PriorityBlockingQueue是一個無界隊列,構造PriorityBlockingQueue時能夠指定初始容量,但這並不意味着PriorityBlockingQueue是有界的,它會在隊列滿時自動擴容。因此須要特別注意因爲控制邏輯不嚴謹致使內存溢出的風險。
另外,使用PriorityBlockingQueue的迭代器遍歷隊列時,你會發現隊列元素是亂序的(與插入順序不一樣)。事實上PriorityBlockingQueue只保證依次從隊頭取出元素是按照優先級排序的(參考最小堆的實現);隊列也不保證兩個相同優先級元素的順序,他們可能以任意順序返回。
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接口便可,無需每一個線程都引入一個計時器去定時觸發事件的發生。
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會讓你的編程更加輕鬆。
多線程協做編程,須要注意線程安全問題。使用JDK自帶的併發編程組件,可讓多線程編程更加輕鬆和安全。本文總結了十大JDK中常見的併發編程組件的典型用法和適用場景,但願對你們有所幫助。