常用Java并發數據結構


總結一下在多線程模式下,常用的一些數據結構.

CountDownLatch - 同步工具類

CountDownLatch這個類能夠使一個線程等待其他線程完成各自的工作后再執行。例如,應用程序的主線程希望在負責啟動框架服務的線程已經啟動所有的框架服務之后再執行。或者希望若發生了獲取某個變量,必須等某異步線程做完了才能獲取到, 否則阻塞.

方法

  // 初始化
  private CountDownLatch mReplySequenceLatch = new CountDownLatch(1);
  // 計數-1
  mReplySequenceLatch.countDown();
  // 等待
  mReplySequenceLatch.await();

CountDownLatch正常就用上面幾個方法,一個初始化等待的線程數,然后每個線程運行結束時countDown一下, 在需要阻塞的地方await即可.

我們舉個真實用例,來自Google zxing的源碼.

final class DecodeThread extends Thread {

  public static final String BARCODE_BITMAP = "barcode_bitmap";

  private final CaptureActivity activity;
  private final Hashtable<DecodeHintType,Object> hints;
  private Handler handler;
  private final CountDownLatch handlerInitLatch;

  DecodeThread(CaptureActivity activity,
               String characterSet,
               ResultPointCallback resultPointCallback) {

    this.activity = activity;
    handlerInitLatch = new CountDownLatch(1);

    hints = new Hashtable<DecodeHintType,Object>();
    Vector<BarcodeFormat> formats = new Vector<BarcodeFormat>();
    formats.add(BarcodeFormat.QR_CODE);
    hints.put(DecodeHintType.POSSIBLE_FORMATS, formats);

    if (characterSet != null) {
      hints.put(DecodeHintType.CHARACTER_SET, characterSet);
    }
    hints.put(DecodeHintType.NEED_RESULT_POINT_CALLBACK, resultPointCallback);
  }

  // 若已經countdown過了,則直接return
  Handler getHandler() {
    try {
      handlerInitLatch.await();                         
    } catch (InterruptedException ie) {
      // continue?
    }
    return handler;
  }

  @Override
  public void run() {
    Looper.prepare();
    handler = new DecodeHandler(activity, hints);
    //
    handlerInitLatch.countDown();                        
    Looper.loop();
  }
}

上述使用CountDownLatch做到了當該類被實類化之后,直接去getHandler是會阻塞的.只有等到運行了,變量準備好了,獲取方法才會返回,這保證了get肯定能拿到對象,拿不到的情況就在等待.有點類似阻塞單列模式的味道.

關于更多CountDownLatch


BlockingQueue-阻塞隊列

來自concurrent包, 多線程編程時經常用到,尤其是任務分配,或者生產者消費者這種類型的.

BlockingQueue的核心方法:

  • 放入數據:

      offer(anObject):表示如果可能的話,將anObject加到BlockingQueue里,即如果BlockingQueue可以容納,
        則返回true,否則返回false.(本方法不阻塞當前執行方法的線程)

      offer(E o, long timeout, TimeUnit unit),可以設定等待的時間,如果在指定的時間內,還不能往隊列中
        加入BlockingQueue,則返回失敗。

      put(anObject):把anObject加到BlockingQueue里,如果BlockQueue沒有空間,則調用此方法的線程被阻斷
        直到BlockingQueue里面有空間再繼續.

  • 獲取數據:

      poll(time):取走BlockingQueue里排在首位的對象,若不能立即取出,則可以等time參數規定的時間,
        取不到時返回null;

      poll(long timeout, TimeUnit unit):從BlockingQueue取出一個隊首的對象,如果在指定時間內,
        隊列一旦有數據可取,則立即返回隊列中的數據。否則知道時間超時還沒有數據可取,返回失敗。

      take():取走BlockingQueue里排在首位的對象,若BlockingQueue為空,阻斷進入等待狀態直到
        BlockingQueue有新的數據被加入;

      drainTo():一次性從BlockingQueue獲取所有可用的數據對象(還可以指定獲取數據的個數),
        通過該方法,可以提升獲取數據效率;不需要多次分批加鎖或釋放鎖。

常用:

  • ArrayBlockingQueue

  • LinkedBlockingQueue

    于鏈表的阻塞隊列,同ArrayListBlockingQueue類似,其內部也維持著一個數據緩沖隊列(該隊列由一個鏈表構成),當生產者往隊列中放入一個數據時,隊列會從生產者手中獲取數據,并緩存在隊列內部,而生產者立即返回;只有當隊列緩沖區達到最大值緩存容量時(LinkedBlockingQueue可以通過構造函數指定該值),才會阻塞生產者隊列

  • PriorityBlockingQueue

    基于優先級的阻塞隊列(優先級的判斷通過構造函數傳入的Compator對象來決定),但需要注意的是PriorityBlockingQueue并不會阻塞數據生產者,而只會在沒有可消費的數據時,阻塞數據的消費者。因此使用的時候要特別注意,生產者生產數據的速度絕對不能快于消費者消費數據的速度,否則時間一長,會最終耗盡所有的可用堆內存空間。


ConcurrentLinkedQueue-非阻塞隊列

BlockingQueue系列對應的是主要是同步操作,是阻塞的,而ConcurrentLinkedQueue是非阻塞的,Queue中元素按FIFO原則進行排序.采用CAS操作,來保證元素的一致性。我們可以根據是否需要阻塞選擇使用哪個數據結構.

ConcurrentLinkedQueue應該注意的地方:

  1. 并不是使用ConcurrentLinkedQueue類之后意味著不需要自己進行任何同步或加鎖操作,查了下資料,
    如果直接使用它提供的函數,比如:queue.add(obj); 或者 queue.poll(obj);,這樣我們自己不需要做任何同步。
    但如果是非原子操作,比如:
if(!queue.isEmpty()) {
    queue.poll(obj);
}

我們很難保證,在調用了isEmpty()之后,poll()之前,這個queue沒有被其他線程修改。
所以對于這種情況,我們還是需要自己同步:

synchronized(queue) {
   if(!queue.isEmpty()) {
     queue.poll(obj);
   }
  }

當然,如果是可以接受的臟讀同樣可以不用加synchronized

  1. 判斷是否還有元素時,ConcurrentLinkedQueue的API原來.size()是要遍歷一遍集合的,比較慢,所以盡量要避免用size而改用isEmpty().

ConcurrentHashMap

一個經常被使用的數據結構,因為HashMap的線程不安全,以及Hashtable的低效,,相比于Hashtable以及Collections.synchronizedMap(),ConcurrentHashMap在線程安全的基礎上提供了更好的寫并發能力,但同時降低了對讀一致性的要求.

ConcurrentHashMap代碼中可以看出,它引入了一個“分段鎖”的概念,具體可以理解為把一個大的Map拆分成N個小的HashTable,根據key.hashCode()來決定把key放到哪個HashTable中。

在ConcurrentHashMap中,就是把Map分成了N個Segment,put和get的時候,都是現根據key.hashCode()算出放到哪個Segment中.默認是16個段.

concurrentHashmap維護一個segment數組,將元素分成若干段(第一次hash)

/**
* The segments, each of which is a specialized hash table.
*/
final Segment<K,V>[] segments;
segments的每一個segment維護一個鏈表數組

在大并發的情況下,只會影響某一個segment的rehash而其他segment不會受到影響


Collections.synchronized類方法

    Collection c = Collections.synchronizedCollection(new ArrayList());
    List list = Collections.synchronizedList(new ArrayList());
    Set set = Collections.synchronizedSet(new HashSet());
    Map map = Collections.synchronizedMap(new HashMap());

返回一個線程安全的集合類.

不過需要注意的是, 這些類的同步也只是靠其內部的一個鎖來控制的,所以若有需要連續的鎖控制的地方,我們還是得自己進行同步控制.說實話這個方法我不常用,因為任何能用到它的地方好像都有方案代替.

參考文章:Collections.synchronizedList()不同鎖造成的陷阱


ThreadLocal<T> 為每個線程創建一個單獨的變量副本,提供了保持對象的方法和避免參數傳遞的復雜性

以前的Java版本是通過, Thread的ThreadLocal.Values變量來做的,保持每個線程有獨立的變量.
jdk1.7開始,改為ThreadLocalMap.即通過為每個線程實現ThreadLocalMap來實現每個線程有獨立變量.

  • void set(Object value)設置當前線程的線程局部變量的值。
  • public Object get()該方法返回當前線程所對應的線程局部變量。
  • public void remove()將當前線程局部變量的值刪除,目的是為了減少內存的占用,該方法是JDK 5.0新增的方法。需要指出的是,當線程結束后,對應該線程的局部變量將自動被垃圾回收,所以顯式調用該方法清除線程的局部變量并不是必須的操作,但它可以加快內存回收的速度。
  • protected Object initialValue()返回該線程局部變量的初始值,該方法是一個protected的方法,顯然是為了讓子類覆蓋而設計的。這個方法是一個延遲調用方法,在線程第1次調用get()或set(Object)時才執行,并且僅執行1次,ThreadLocal中的缺省實現直接返回一個null。

ThreadLocal博客


本文作者:Anderson/Jerey_Jobs

博客地址 : http://jerey.cn/

簡書地址 : Anderson大碼渣

github地址 : https://github.com/Jerey-Jobs

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,517評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,087評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,521評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,493評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,207評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,603評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,624評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,813評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,364評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,110評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,305評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,874評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,532評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,953評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,209評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,033評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,268評論 2 375

推薦閱讀更多精彩內容

  • layout: posttitle: 《Java并發編程的藝術》筆記categories: Javaexcerpt...
    xiaogmail閱讀 5,850評論 1 19
  • 一.線程安全性 線程安全是建立在對于對象狀態訪問操作進行管理,特別是對共享的與可變的狀態的訪問 解釋下上面的話: ...
    黃大大吃不胖閱讀 860評論 0 3
  • 一、并發 進程:每個進程都擁有自己的一套變量 線程:線程之間共享數據 1.線程 Java中為多線程任務提供了很多的...
    SeanMa閱讀 2,511評論 0 11
  • Java SE 基礎: 封裝、繼承、多態 封裝: 概念:就是把對象的屬性和操作(或服務)結合為一個獨立的整體,并盡...
    Jayden_Cao閱讀 2,129評論 0 8
  • 今天事情很多,明天要早起趕火車現在還沒有收拾完,又聽聞一青年同事白血病醫冶無效去世的惡耗,??心情很低落,感慨命運無...
    Kitty北京閱讀 181評論 0 0