學習了一段時間J.U.C,打算做個小結,個人感覺總結還是非常重要,要不然總感覺知識點零零散散的。
有錯誤也歡迎指正,大家共同進步;
另外,轉載請注明鏈接,寫篇文章不容易啊,http://www.cnblogs.com/chenpi/p/5614290.html
本文目錄如下,基本上涵蓋了J.U.C的主要內容;
JSR,全稱 Java Specification Requests, 即Java規范提案, 主要是用於向JCP(Java Community Process)提出新增標准化技術規范的正式請求。每次JAVA版本更新都會有對應的JSR更新,比如在Java 8版本中,其新特性Lambda表達式對應的是JSR 335,新的日期和時間API對應的是JSR 310。
當然,本文的關注點僅僅是JSR 166,它是一個關於Java並發編程的規范提案,在JDK中,該規范由java.util.concurrent包實現,是在JDK 5.0的時候被引入的;
另外JDK6引入Deques、Navigable collections,對應的是JSR 166x,JDK7引入fork-join框架,用於並行執行任務,對應的是JSR 166y。
即java.util.concurrent的縮寫,該包參考自EDU.oswego.cs.dl.util.concurrent,是JSR 166標准規范的一個實現;
那麼,JSR 166以及J.U.C包的作者是誰呢,沒錯,就是Doug Lea大神,挺牛逼的,大神級別任務,貼張照片膜拜下。。。
簡單的說,就是一個任務的執行和調度框架,涉及的類如下圖所示:
其中,最頂層是Executor接口,它的定義很簡單,一個用於執行任務的execute方法,如下所示:
public interface Executor { void execute(Runnable command); }
另外,我們還可以看到一個Executors類,它是一個工具類(有點類似集合框架的Collections類),用於創建ExecutorService
、ScheduledExecutorService
、ThreadFactory
和 Callable對象。
任務的提交過程與執行過程解耦,用戶只需定義好任務提交,具體如何執行,什麼時候執行不需要關心;
定義好任務(如Callable對象),把它提交給ExecutorService(如線程池)去執行,得到Future對象,然後調用Future的get方法等待執行結果即可。
實現Callable接口或Runnable接口的類,其實例就可以成為一個任務提交給ExecutorService去執行;
其中Callable任務可以返回執行結果,Runnable任務無返回結果;
通過Executors工具類可以創建各種類型的線程池,如下為常見的四種:
ExecutorService executor = Executors.newCachedThreadPool();//創建線程池 Task task = new Task(); //創建Callable任務 Future<Integer> result = executor.submit(task);//提交任務給線程池執行 result.get();//等待執行結果; 可以傳入等待時間參數,指定時間內沒返回的話,直接結束
方式一:首先定義任務集合,然後定義Future集合用於存放執行結果,執行任務,最後遍歷Future集合獲取結果;
方式二:首先定義任務集合,通過CompletionService包裝ExecutorService,執行任務,然後調用其take()方法去取Future對象
這裡稍微解釋下,在方式一中,從集合中遍歷的每個Future對象並不一定處於完成狀態,這時調用get()方法就會被阻塞住,所以後面的任務即使已完成也不能得到結果;而方式二中,CompletionService的實現是維護一個保存Future對象的BlockingQueue,只有當這個Future對象狀態是結束的時候,才會加入到這個Queue中,所以調用take()能從阻塞隊列中拿到最新的已完成任務的結果;
AQS框架是J.U.C中實現鎖及同步機制的基礎,其底層是通過調用 LockSupport .unpark()和 LockSupport .park()實現線程的阻塞和喚醒。
AbstractQueuedSynchronizer是一個抽象類,主要是維護了一個int類型的state屬性和一個非阻塞、先進先出的線程等待隊列;其中state是用volatile修飾的,保證線程之間的可見性,隊列的入隊和出對操作都是無鎖操作,基於自旋鎖和CAS實現;另外AQS分為兩種模式:獨占模式和共享模式,像ReentrantLock是基於獨占模式模式實現的,CountDownLatch、CyclicBarrier等是基於共享模式。
非公平鎖的lock方法的實現:
final void lock() { //CAS操作,如果State為0(表示當前沒有其它線程占有該鎖),則將它設置為1 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
首先是不管先後順序,直接嘗試獲取鎖(非公平的體現),成功的話,直接獨占訪問;
如果獲取鎖失敗,則調用AQS的acquire方法,在該方法內部會調用tryAcquire方法再次嘗試獲取鎖以及是否可重入判斷,如果失敗,則掛起當前線程並加入到等待隊列;
具體可查看ReentrantLock.NonfairSync類和AbstractQueuedSynchronizer類對應的源碼。
先看一下Lock接口提供的主要方法,如下:
Lock
實例上的 Condition
實例關於Lock接口的實現,我們主要是關注以下兩個類:
ReentrantLock
可重入鎖,所謂的可重入鎖,也叫遞歸鎖,是指一個線程獲取鎖後,再次獲取該鎖時,不需要重新等待獲取。ReentrantLock分為公平鎖和非公平鎖,公平鎖指的是嚴格按照先來先得的順序排隊等待去獲取鎖,而非公平鎖每次獲取鎖時,是先直接嘗試獲取鎖,獲取不到,再按照先來先得的順序排隊等待。
注意:ReentrantLock和synchronized都是可重入鎖。
ReentrantReadWriteLock
可重入讀寫鎖,指的是沒有線程進行寫操作時,多個線程可同時進行讀操作,當有線程進行寫操作時,其它讀寫操作只能等待。即“讀-讀能共存,讀-寫不能共存,寫-寫不能共存”。
在讀多於寫的情況下,讀寫鎖能夠提供比排它鎖更好的並發性和吞吐量。
Condition
Condition對象是由Lock對象創建的,一個Lock對象可以創建多個Condition,其實Lock和Condition都是基於AQS實現的。
Condition對象主要用於線程的等待和喚醒,在JDK 5之前,線程的等待喚醒是用Object對象的wait/notify/notifyAll方法實現的,使用起來不是很方便;
在JDK5之後,J.U.C包提供了Condition,其中:
Condition.await對應於Object.wait;
Condition.signal 對應於 Object.notify;
Condition.signalAll 對應於 Object.notifyAll;
使用Condition對象有一個比較明顯的好處是一個鎖可以創建多個Condition對象,我們可以給某類線程分配一個Condition,然後就可以喚醒特定類的線程。
J.U.C中的同步器主要用於協助線程同步,有以下四種:
閉鎖主要用於讓一個主線程等待一組事件發生後繼續執行,這裡的事件其實就是指CountDownLatch對象的countDown方法。注意其它線程調用完countDown方法後,是會繼續執行的,具體如下圖所示:
在CountDownLatch內部,包含一個計數器,一開始初始化為一個整數(事件個數),發生一個事件後,調用countDown方法,計數器減1,await用於等待計數器為0後繼續執行當前線程;
如上圖:TA主線程會一直等待,直到計數cnt=0,才繼續執行,
可參照之前寫的一篇文章,如下鏈接,裡面有一個閉鎖的demo示例。
http://www.cnblogs.com/chenpi/p/5358579.html
柵欄主要用於等待其它線程,且會阻塞自己當前線程,所有線程必須同時到達柵欄位置後,才能繼續執行;且在所有線程到達柵欄處,可以觸發執行另外一個預先設置的線程,具體如下圖所示:
在上圖中,T1、T2、T3每調用一次await,計數減減,且在它們調用await方法的時候,如果計數不為0,會阻塞自己的線程;
另外,TA線程會在所有線程到達柵欄處(計數為0)的時候,才開始執行;
可參照之前寫的一篇文章,如下鏈接,裡面有一個柵欄的demo示例。
http://www.cnblogs.com/chenpi/p/5358579.html
信號量主要用於控制訪問資源的線程個數,常常用於實現資源池,如數據庫連接池,線程池...
在Semaphore中,acquire方法用於獲取資源,有的話,繼續執行(使用結束後,記得釋放資源),沒有資源的話將阻塞直到有其它線程調用release方法釋放資源;
可參照之前寫的一篇文章,如下鏈接,裡面有一個信號量的demo示例。
http://www.cnblogs.com/chenpi/p/5358579.html
交換器主要用於線程之間進行數據交換;
當兩個線程都到達共同的同步點(都執行到exchanger.exchange的時刻)時,發生數據交換,否則會等待直到其它線程到達;
原子變量主要是方便程序員在多線程環境下,無鎖的進行原子操作;
原子類是基於Unsafe實現的包裝類,核心操作是CAS原子操作;所謂的CAS操作,即compare and swap,指的是將預期值與當前變量的值比較(compare),如果相等則使用新值替換(swap)當前變量,否則不作操作;我們可以摘取一段AtomicInteger的源碼,如下:
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
在compareAndSwapInt方法中,valueOffset是內存地址,expect是預期值,update是更新值,如果valueOffset地址處的值與預期值相等,則將valueOffset地址處的值更新為update值。PS:現代CPU已廣泛支持CAS指令;
在Java中,有四種原子更新方式,如下:
更多關於原子變量,可以參考之前寫的三篇文章:
atomic包 :http://www.cnblogs.com/chenpi/p/5375805.html
AtomicInteger源碼注釋:http://www.cnblogs.com/chenpi/p/5357136.html
理解sun.misc.Unsafe:http://www.cnblogs.com/chenpi/p/5389254.html
提個醒:簡單的自增操作,如i++,並不是一個原子操作,不過使用原子變量類進行操作,如調用incrementAndGet()方法進行自增,可以使其成為原子操作;
阻塞隊列提供了可阻塞的入隊和出對操作,如果隊列滿了,入隊操作將阻塞直到有空間可用,如果隊列空了,出隊操作將阻塞直到有元素可用;
在Java中,主要有以下類型的阻塞隊列:
阻塞隊列有一個比較典型的應用場景是解決生產者-消費者問題,具體可以參考之前寫的一篇文章,裡面有demo示例:
使用阻塞隊列解決生產者-消費者問題 : http://www.cnblogs.com/chenpi/p/5553325.html
接下來,我們來看一下工作中比較常見的一塊內容,並發容器;
說到並發容器,不得不提同步容器,在JDK5之前,為了線程安全,我們一般都是使用同步容器,同步容器主要有以下缺點:
對於復合操作,我們可以舉個例子, 因為比較容易被忽視,如下代碼:
public static Integer getLast(Vector<Integer> list){ int lastIndex = list.size() - 1; if(lastIndex < 0) return null; return list.get(lastIndex); }
在以上代碼中,雖然list集合是Vector類型,但該方法仍然不是原子操作,因為在list.size()和list.get(lastIndex)之間,可能已經發生了很多事。
那麼,在JDK 5之後,有哪些並發容器呢,這裡主要說兩種,如下:
ConcurrentHashMap
ConcurrentHashMap是采用分離鎖技術,在同步容器中,是一個容器一個鎖,但在ConcurrentHashMap中,會將hash表的數組部分分成若干段,每段維護一個鎖;這些段可以並發的進行寫操作,以達到高效的並發訪問,如下圖示例:
另外,性能是我們比較關心的,我們可以與同步容器做個對比,如下圖所示,PS:該圖資料來自參考內容~,我自身沒做過測試:
CopyOnWriteArrayList/Set
也叫拷貝容器,指的是寫數據的時候,重新拷貝一份進行寫操作,完成後,再將原容器的引用指向新的拷貝容器。
適用情況:當讀操作遠遠大於寫操作的時候,考慮用這個並發集合。
這塊內容是在JDK7中引入的,個人覺得相當牛逼,可以方便利用多核平台的計算能力,簡化並行程序的編寫,開發人員僅需關注如何劃分任務和組合中間結果。
fork/join框架的核心是ForkJoinPool類,實現了工作竊取算法(對那些處理完自身任務的線程,會從其它線程竊取任務執行)並且能夠執行 ForkJoinTask任務。
適用場景:大任務能被遞歸拆分成多個子任務的應用; 可以參考下圖,幫助理解,位於圖上部的 Task 依賴於位於其下的 Task 的執行,只有當所有的子任務都完成之後,調用者才能獲得 Task 0 的返回結果。其實這是一種分而治之的思想:其實對於使用fork/join框架的開發人員來說,主要任務還是在於任務劃分,可以參考如下偽代碼:
if (任務足夠小){ 直接執行該任務; }else{ 將任務拆分成多個子任務; 執行這些子任務並等待結果; }
具體可以參考之前寫的一篇文章,裡面有一個使用fork/join框架進行圖片水平模糊的例子:
JAVA中的Fork/Join框架 :http://www.cnblogs.com/chenpi/p/5581198.htmlTimeUnit是java.util.concurrent包下面的一個枚舉類,TimeUnit提供了可讀性更好的線程暫停操作。
在JDK5之前,一般我們暫停線程是這樣寫的:
Thread.sleep(2400000)//可讀性差
可讀性相當的差,一眼看去,不知道睡了多久;
在JDK5之後,我們可以這樣寫:
TimeUnit.SECONDS.sleep(4); TimeUnit.MINUTES.sleep(4); TimeUnit.HOURS.sleep(1); TimeUnit.DAYS.sleep(1);
清晰明了;
另外,TimeUnit還提供了便捷方法用於把時間轉換成不同單位,例如,如果你想把秒轉換成毫秒,你可以使用下面代碼
TimeUnit.SECONDS.toMillis(44);// 44,000
《並發編程實戰》
《JAVA 編程思想-4版》
谷歌,百度
並發編程網
http://tutorials.jenkov.com/
http://howtodoinjava.com/category/core-java/multi-threading/
http://www.infoq.com/cn/articles/jdk1.8-abstractqueuedsynchronizer
https://zh.wikipedia.org/wiki/JCP
http://geekrai.blogspot.com/2013/07/executor-framework-in-java.html