程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> JAVA並發編程J.U.C學習總結,java並發編程j.u.c

JAVA並發編程J.U.C學習總結,java並發編程j.u.c

編輯:JAVA綜合教程

JAVA並發編程J.U.C學習總結,java並發編程j.u.c


前言  

學習了一段時間J.U.C,打算做個小結,個人感覺總結還是非常重要,要不然總感覺知識點零零散散的。

有錯誤也歡迎指正,大家共同進步;

另外,轉載請注明鏈接,寫篇文章不容易啊,http://www.cnblogs.com/chenpi/p/5614290.html

本文目錄如下,基本上涵蓋了J.U.C的主要內容;

  • JSR 166及J.U.C
  • Executor框架(線程池、 Callable 、Future)
  • AbstractQueuedSynchronizer(AQS框架)
  • Locks & Condition(鎖和條件變量)
  • Synchronizers(同步器)
  • Atomic Variables (原子變量)
  • BlockingQueue(阻塞隊列)
  • Concurrent Collections(並發容器)
  • Fork/Join並行計算框架
  • TimeUnit枚舉
  • 參考資料

JSR 166及J.U.C

什麼是JSR:

JSR,全稱 Java Specification Requests, 即Java規范提案, 主要是用於向JCP(Java Community Process)提出新增標准化技術規范的正式請求。每次JAVA版本更新都會有對應的JSR更新,比如在Java 8版本中,其新特性Lambda表達式對應的是JSR 335,新的日期和時間API對應的是JSR 310。

什麼是JSR 166:

當然,本文的關注點僅僅是JSR 166,它是一個關於Java並發編程的規范提案,在JDK中,該規范由java.util.concurrent包實現,是在JDK 5.0的時候被引入的;

另外JDK6引入Deques、Navigable collections,對應的是JSR 166x,JDK7引入fork-join框架,用於並行執行任務,對應的是JSR 166y。

什麼是J.U.C:

即java.util.concurrent的縮寫,該包參考自EDU.oswego.cs.dl.util.concurrent,是JSR 166標准規范的一個實現;

膜拜

那麼,JSR 166以及J.U.C包的作者是誰呢,沒錯,就是Doug Lea大神,挺牛逼的,大神級別任務,貼張照片膜拜下。。。

Executor框架(線程池、 Callable 、Future)

什麼是Executor框架

簡單的說,就是一個任務的執行和調度框架,涉及的類如下圖所示:

其中,最頂層是Executor接口,它的定義很簡單,一個用於執行任務的execute方法,如下所示:

public interface Executor {
    void execute(Runnable command);
}

 另外,我們還可以看到一個Executors類,它是一個工具類(有點類似集合框架的Collections類),用於創建ExecutorServiceScheduledExecutorServiceThreadFactoryCallable對象。

優點:

任務的提交過程與執行過程解耦,用戶只需定義好任務提交,具體如何執行,什麼時候執行不需要關心;

典型步驟:

定義好任務(如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()能從阻塞隊列中拿到最新的已完成任務的結果;

AbstractQueuedSynchronizer (AQS框架)

什麼是AQS框架

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類對應的源碼。

Locks & Condition(鎖和條件變量)

先看一下Lock接口提供的主要方法,如下:

  • lock()  等待獲取鎖
  • lockInterruptibly()  可中斷等待獲取鎖,synchronized無法實現可中斷等待
  • tryLock() 嘗試獲取鎖,立即返回true或false
  • tryLock(long time, TimeUnit unit)    指定時間內等待獲取鎖
  • unlock()      釋放鎖
  • newCondition()   返回一個綁定到此 Lock 實例上的 Condition 實例

關於Lock接口的實現,我們主要是關注以下兩個類:

  • ReentrantLock
  • ReentrantReadWriteLock

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,然後就可以喚醒特定類的線程。

Synchronizers(同步器)

J.U.C中的同步器主要用於協助線程同步,有以下四種:

閉鎖 CountDownLatch

閉鎖主要用於讓一個主線程等待一組事件發生後繼續執行,這裡的事件其實就是指CountDownLatch對象的countDown方法。注意其它線程調用完countDown方法後,是會繼續執行的,具體如下圖所示:

在CountDownLatch內部,包含一個計數器,一開始初始化為一個整數(事件個數),發生一個事件後,調用countDown方法,計數器減1,await用於等待計數器為0後繼續執行當前線程;

如上圖:TA主線程會一直等待,直到計數cnt=0,才繼續執行,

可參照之前寫的一篇文章,如下鏈接,裡面有一個閉鎖的demo示例。

http://www.cnblogs.com/chenpi/p/5358579.html

柵欄 CyclicBarrier

柵欄主要用於等待其它線程,且會阻塞自己當前線程,所有線程必須同時到達柵欄位置後,才能繼續執行;且在所有線程到達柵欄處,可以觸發執行另外一個預先設置的線程,具體如下圖所示:

在上圖中,T1、T2、T3每調用一次await,計數減減,且在它們調用await方法的時候,如果計數不為0,會阻塞自己的線程;

另外,TA線程會在所有線程到達柵欄處(計數為0)的時候,才開始執行;

可參照之前寫的一篇文章,如下鏈接,裡面有一個柵欄的demo示例。

http://www.cnblogs.com/chenpi/p/5358579.html

信號量Semaphore

信號量主要用於控制訪問資源的線程個數,常常用於實現資源池,如數據庫連接池,線程池...

在Semaphore中,acquire方法用於獲取資源,有的話,繼續執行(使用結束後,記得釋放資源),沒有資源的話將阻塞直到有其它線程調用release方法釋放資源;

可參照之前寫的一篇文章,如下鏈接,裡面有一個信號量的demo示例。

http://www.cnblogs.com/chenpi/p/5358579.html

交換器 Exchanger

交換器主要用於線程之間進行數據交換;

當兩個線程都到達共同的同步點(都執行到exchanger.exchange的時刻)時,發生數據交換,否則會等待直到其它線程到達;

Atomic Variables(原子變量)

原子變量主要是方便程序員在多線程環境下,無鎖的進行原子操作;

原子類是基於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中,有四種原子更新方式,如下:

  • 原子方式更新基本類型; AtomicInteger 、 AtomicLong 等
  • 原子方式更新數組; AtomicIntegerArray、 AtomicLongArray等
  • 原子方式更新引用; AtomicReference、 AtomicReferenceFieldUpdater…
  • 原子方式更新字段; AtomicIntegerFieldUpdater、 AtomicStampedReference(解決CAS的ABA問題)…

更多關於原子變量,可以參考之前寫的三篇文章:

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()方法進行自增,可以使其成為原子操作;

BlockingQueue(阻塞隊列)

阻塞隊列提供了可阻塞的入隊和出對操作,如果隊列滿了,入隊操作將阻塞直到有空間可用,如果隊列空了,出隊操作將阻塞直到有元素可用;

在Java中,主要有以下類型的阻塞隊列:

  • ArrayBlockingQueue :一個由數組結構組成的有界阻塞隊列。
  • LinkedBlockingQueue :一個由鏈表結構組成的有界阻塞隊列。
  • PriorityBlockingQueue :一個支持優先級排序的無界阻塞隊列。
  • DelayQueue:一個支持延時獲取元素的無界阻塞隊列。
  • SynchronousQueue:一個不存儲元素的阻塞隊列。
  • LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列。
  • LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列。

阻塞隊列有一個比較典型的應用場景是解決生產者-消費者問題,具體可以參考之前寫的一篇文章,裡面有demo示例:

使用阻塞隊列解決生產者-消費者問題 : http://www.cnblogs.com/chenpi/p/5553325.html

Concurrent Collections(並發容器)

接下來,我們來看一下工作中比較常見的一塊內容,並發容器;

說到並發容器,不得不提同步容器,在JDK5之前,為了線程安全,我們一般都是使用同步容器,同步容器主要有以下缺點:

  • 同步容器對所有容器狀態的訪問都串行化,嚴重降低了並發性;
  • 某些復合操作,仍然需要加鎖來保護
  • 迭代期間,若其它線程並發修改該容器,會拋出ConcurrentModificationException異常,即快速失敗機制

對於復合操作,我們可以舉個例子, 因為比較容易被忽視,如下代碼:

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
  • CopyOnWriteArrayList/Set

ConcurrentHashMap

ConcurrentHashMap是采用分離鎖技術,在同步容器中,是一個容器一個鎖,但在ConcurrentHashMap中,會將hash表的數組部分分成若干段,每段維護一個鎖;這些段可以並發的進行寫操作,以達到高效的並發訪問,如下圖示例:

另外,性能是我們比較關心的,我們可以與同步容器做個對比,如下圖所示,PS:該圖資料來自參考內容~,我自身沒做過測試:

CopyOnWriteArrayList/Set

也叫拷貝容器,指的是寫數據的時候,重新拷貝一份進行寫操作,完成後,再將原容器的引用指向新的拷貝容器。

適用情況:當讀操作遠遠大於寫操作的時候,考慮用這個並發集合。

Fork/Join並行計算框架

這塊內容是在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.html

TimeUnit枚舉

TimeUnit是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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved