Java 多線程(八)- 其他常用并發(fā)容器

CopyOnWriteArrayList

CopyOnWriteArrayList 是寫時復(fù)制的容器。通俗的理解是當我們要往容器中添加元素的時候叭首,不直接往當前的數(shù)組天假蜜托,而是將當前數(shù)組進行 Copy魏割,然后往新的數(shù)組中添加元素,添加完元素后,再將數(shù)組引用指向新的數(shù)組眨八。

這樣做的好處是可以并發(fā)的讀,而不需要加鎖同步左电,而同步鎖只加在寫線程上廉侧,也就說很可能讀和寫并不是同一個數(shù)組。

實現(xiàn)原理

簡單起見券腔,不妨看看 CopyOnWriteArrayList 的 get 和 add 接口是如何實現(xiàn)的:

final transient ReentrantLock lock = new ReentrantLock();

/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;

private E get(Object[] a, int index) {
    return (E) a[index];
}

public E get(int index) {
    return get(getArray(), index);
}

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

可以看到調(diào)用 get 讀數(shù)據(jù)的時候并沒有加鎖伏穆,多個線程是并發(fā)的讀。但是調(diào)用 add 接口是會先獲取鎖纷纫,所有寫線程串行訪問枕扫。但是寫線程和讀線程是并發(fā)訪問的,所以如果寫線程還在寫的時候辱魁,讀線程還是有可能讀取舊的數(shù)據(jù)烟瞧。

CopyOnWriteArrayList 返回的迭代器不會拋出 ConcurrentModificationException 異常诗鸭,并且返回的元素與迭代器創(chuàng)建時的元素完全一致,而不必考慮之后修改操作所帶來的影響参滴。

應(yīng)用場景

每當修改時强岸,CopyOnWriteArrayList 都會復(fù)制底層數(shù)組,這需要一定開銷砾赔,特別是當容器規(guī)模非常大的蝌箍。僅當?shù)僮?strong>遠遠多于修改操作時,才應(yīng)該考慮使用“寫入時復(fù)制”容器暴心。

盡量使用批量添加妓盲。因為每次添加,容器每次都會進行復(fù)制专普,所以減少添加次數(shù)悯衬,可以減少容器的復(fù)制次數(shù)。

缺點

CopyOnWriteArrayList 有很多優(yōu)點檀夹,但是同時也存在兩個問題筋粗,即內(nèi)存占用問題數(shù)據(jù)一致性問題。所以在開發(fā)的時候需要注意一下:

  • 內(nèi)存占用問題炸渡。因為寫時復(fù)制機制娜亿,所以在進行寫操作的時候,內(nèi)存里會同時駐扎兩個數(shù)組偶摔,如果數(shù)組內(nèi)對象占用的內(nèi)存比較暇唾,那么這個時候很有可能造成頻繁的 GC。
  • 只能保證數(shù)據(jù)的最終一致性辰斋,不能保證數(shù)據(jù)的實時一致性策州。所以如果你希望寫入的的數(shù)據(jù),馬上能讀到宫仗,請不要使用寫時復(fù)制容器够挂。
CopyOnWriteArraySet

CopyOnWriteArraySet 實現(xiàn) Set 接口,也屬于寫時復(fù)制容器藕夫,其實 CopyOnWriteArraySet 內(nèi)部是用
CopyOnWriteArrayList 實現(xiàn)的孽糖,所以 CopyOnWriteArraySet 的特性幾乎和 CopyOnWriteArrayList 一樣,簡單起見毅贮,可以看看以下關(guān)鍵代碼:

private final CopyOnWriteArrayList<E> al;

public boolean add(E e) {
    return al.addIfAbsent(e);
}

public Object[] toArray() {
    return al.toArray();
}

BlockingQueue

BlockingQueue 繼承自 Queue办悟,Queue 繼承自 Collection,BlockingQueue 提供了可阻塞的 put 和 take 方法滩褥,以及支持定時的 offer 和 poll 方法病蛉。如果隊列已經(jīng)滿了,那么 put 方法將阻塞直到有空間可用;如果隊列為空铺然,那么 take 方法將會阻塞知道有元素可用俗孝。隊列可以是有界的也可以是無界的,無界隊列永遠不會充滿魄健,所以無界隊列的 put 方法也永遠不會阻塞赋铝。

接口

BlockingQueue 主要方法如下所示:

//添加元素,添加成功則返回 true沽瘦,隊列已滿革骨,則拋出 IllegalStateException 異常
boolean add(E e);

//添加元素,添加成功返回 true其垄,添加失敗 返回 false
boolean offer(E e); 

//添加元素苛蒲,如果隊列已滿卤橄,則阻塞等待
void put(E e) throws InterruptedException;
    
//添加元素绿满,如果隊列已滿,則阻塞等待一段時間窟扑,等待后還是插入失敗喇颁,則返回 false
boolean offer(E e, long timeout, TimeUnit unit)
    throws InterruptedException;
    
//獲取并刪除隊列第一個元素,如果隊列為空嚎货,則阻塞等待
E take() throws InterruptedException;

//獲取并刪除隊列第一個元素
//如果隊列為空橘霎,則阻塞等待一段時間,等待后還是為空殖属,則返回 null
E poll(long timeout, TimeUnit unit)
    throws InterruptedException;

//獲取并刪除隊列第一個元素姐叁,如果隊列為空,則拋出 NoSuchElementException 異常
E remove();

//獲取并刪除隊列第一個元素洗显,如果隊列為空外潜,則返回 null
E poll();

//獲取隊列第一個元素,但是并不出隊挠唆,如果隊列為空处窥,拋出 NoSuchElementException 異常
E element();

//獲取隊列第一個元素,但是并不出隊玄组,如果隊列為空滔驾,則返回 null
E peek()
使用場景

BlockingQueue 經(jīng)常用于生產(chǎn)者-消費者設(shè)計模式中。該模式將“找出需要完成的工作”與“執(zhí)行工作”這兩個過程分離開來俄讹。生產(chǎn)者-消費者模式能夠簡化開發(fā)過程哆致,因為它能夠消除生產(chǎn)者類和消費者類之間的代碼依賴性。此外患膛,該模式還將生產(chǎn)數(shù)據(jù)的過程與使用數(shù)據(jù)的過程解耦以簡化工作負載的管理摊阀,因為這兩個過程在處理數(shù)據(jù)的速率上有所不同。

一種常見的生產(chǎn)者-消費者模式就是線程池與工作隊列的組合,在 Executor 任務(wù)執(zhí)行框架中就體現(xiàn)了這種模式驹溃。

ArrayBlockingQueue

它是一個由數(shù)組支持的有界阻塞隊列城丧。它的容納大小是固定的。此隊列按 FIFO(先進先出)原則對元素進行排序豌鹤。數(shù)組容量在構(gòu)造函數(shù)里傳入亡哄。以下是它的核心成員變量:

/** 數(shù)組作為隊列*/
final Object[] items;
/** 隊列頭部坐標 */
int takeIndex;
/** 隊尾坐標 **/
int putIndex;
/** 隊列中元素個數(shù) **/
int count;
/** 獨占鎖,對隊列的訪問需要獲取該鎖布疙,保證線程訪問同步 **/
final ReentrantLock lock;
/** 非空等待條件變量蚊惯,用于阻塞/通知隊列為空時的 take 線程 */
private final Condition notEmpty;
/** 未滿等待條件隊列,用于阻塞/通知隊列充滿時的 put 線程  **/
private final Condition notFull;

ArrayBlockingQueue 有以下特點:

  1. 有界數(shù)組灵临,隊列容量在構(gòu)造時傳入截型;
  2. 通過 takeIndex / putIndex 設(shè)計,數(shù)組是 循環(huán)數(shù)組儒溉;
  3. lock 保護臨界區(qū)宦焦,notEmpty 和 notFull 用來阻塞和通知等待線程;
  4. 不接受 null 元素顿涣;
  5. 構(gòu)造函數(shù)中可以指定公平性波闹,公平性是通過 lock 實現(xiàn)的。
LinkedBlockingQueue

它是一個基于已鏈接節(jié)點的涛碑,可以無界精堕,也可以有界的阻塞隊列。此隊列按 FIFO(先進先出)原則對元素進行排序蒲障。以下是它的核心成員變量:

 /** 隊列容量歹篓,默認為 Integer.MAX_VALUE */
private final int capacity;
/** 當前元素數(shù)量 */
private final AtomicInteger count = new AtomicInteger();
/** 隊列頭部 */
transient Node<E> head;
/** 隊列尾部 */
private transient Node<E> last;
/** 從隊列取數(shù)據(jù)時的獨占鎖 */
private final ReentrantLock takeLock = new ReentrantLock();
/** 條件變量,用于阻塞/喚醒隊列為空時的 take 線程 */
private final Condition notEmpty = takeLock.newCondition();
/** 向隊列加入數(shù)據(jù)時的獨占鎖 */
private final ReentrantLock putLock = new ReentrantLock();
/** 條件變量揉阎,用于阻塞/喚醒隊列充滿時的 put 線程 */
private final Condition notFull = putLock.newCondition();

LinkedBlockingQueue 有以下特點:

  1. 內(nèi)部鏈表結(jié)構(gòu)庄撮,默認無界;
  2. 初始化時余黎,head 和 last 指向同一個占位 node重窟,該 node 的元素為 null;
  3. takeLock 保護多線程同步訪問 head惧财,取數(shù)據(jù)巡扇;
  4. putLock 保護多線程同步訪問 last,往隊列加數(shù)據(jù)垮衷;
  5. 讀線程和寫線程同步分離厅翔,提高隊列的并發(fā)行,同時可以有一個讀線程和寫線程訪問隊列搀突;
  6. 沒有公平性設(shè)計刀闷。
PriorityBlockingQueue

它是一個基于數(shù)組的無界數(shù)組。它使用與類PriorityQueue相同的順序規(guī)則,并且提供了阻塞檢索的操作甸昏。以下是它的核心成員變量:

/** 元素數(shù)組顽分,數(shù)組結(jié)構(gòu),但是是用極小堆來處理的 */
private transient Object[] queue;
/** 隊列當前大小 */
    private transient int size;
    /**
      * 排序使用的對比式施蜜,如果沒有設(shè)置卒蘸,則需要對象實現(xiàn) Comparable 接口 
      */
private transient Comparator<? super E> comparator;
    /**
      * public 接口都要獲取該鎖,用于保護臨界區(qū)
       */
 private final ReentrantLock lock;
 /**
       * 當取數(shù)據(jù)時翻默,隊列為空缸沃,線程等待的條件變量
      */
 private final Condition notEmpty;
    /**
      * 數(shù)組擴容時的自旋鎖.
     */
    private transient volatile int allocationSpinLock;

PriorityBlockingQueue 有以下特點:

  1. 內(nèi)部數(shù)組存儲數(shù)據(jù),讀和寫需要同一個鎖進行同步保護修械;
  2. 雖然是數(shù)組存儲趾牧,但是使用極小堆進行排序;
  3. 初始化可以指定 comparator肯污,如果沒有指定翘单,則需要對象實現(xiàn) Comparable 接口,調(diào)用它的 compareTo;
  4. 不接受 null
  5. 雖然是數(shù)組仇箱,但是必要時回擴容县恕,所以無界,只需要 notEmpty 條件變量
SynchronousQueue

它是一個沒有數(shù)據(jù)緩沖的 BlockingQueue剂桥,生產(chǎn)者線程對其的插入操作 put 必須等待消費者的移除操作take,反過來也一樣属提。

不像 ArrayBlockingQueue 或 LinkedListBlockingQueue权逗,SynchronousQueue 內(nèi)部并沒有數(shù)據(jù)緩存空間,你不能調(diào)用peek()方法來看隊列中是否有數(shù)據(jù)元素冤议,因為數(shù)據(jù)元素只有當你試著取走的時候才可能存在斟薇,不取走而只想偷窺一下是不行的,當然遍歷這個隊列的操作也是不允許的恕酸。隊列頭元素是第一個排隊要插入數(shù)據(jù)的線程堪滨,而不是要交換的數(shù)據(jù)。數(shù)據(jù)是在配對的生產(chǎn)者和消費者線程之間直接傳遞的蕊温,并不會將數(shù)據(jù)緩沖數(shù)據(jù)到隊列中袱箱。可以這樣來理解:生產(chǎn)者和消費者互相等待對方义矛,握手发笔,然后一起離開。

SynchronousQueue 有以下特點:

  1. 沒有數(shù)據(jù)緩沖凉翻,寫線程和讀線程一一對應(yīng)了讨;
  2. 排隊等候的線程,而非數(shù)據(jù);
  3. 支持 FIFO 或 LIFO前计,公平模式下胞谭,使用 TransferQueue 支持 FIFO,非公平模式下男杈,使用 TransferStack 支持 LIFO韭赘;
  4. 內(nèi)部用鏈表存儲等候線程;
  5. 實現(xiàn)時势就,使用輪詢 CAS 來實現(xiàn)無鎖化處理泉瞻;

內(nèi)容來源

Java 并發(fā)編程實戰(zhàn)

http://ifeve.com/java-copy-on-write/

http://wsmajunfeng.iteye.com/blog/1629354

http://jiangzhengjun.iteye.com/blog/683593

http://ifeve.com/java-synchronousqueue/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市苞冯,隨后出現(xiàn)的幾起案子袖牙,更是在濱河造成了極大的恐慌,老刑警劉巖舅锄,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鞭达,死亡現(xiàn)場離奇詭異,居然都是意外死亡皇忿,警方通過查閱死者的電腦和手機畴蹭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鳍烁,“玉大人叨襟,你說我怎么就攤上這事♂;模” “怎么了糊闽?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長爹梁。 經(jīng)常有香客問我右犹,道長,這世上最難降的妖魔是什么姚垃? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任念链,我火速辦了婚禮,結(jié)果婚禮上积糯,老公的妹妹穿的比我還像新娘掂墓。我一直安慰自己,他們只是感情好絮宁,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布梆暮。 她就那樣靜靜地躺著,像睡著了一般绍昂。 火紅的嫁衣襯著肌膚如雪啦粹。 梳的紋絲不亂的頭發(fā)上偿荷,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音唠椭,去河邊找鬼跳纳。 笑死,一個胖子當著我的面吹牛贪嫂,可吹牛的內(nèi)容都是我干的寺庄。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼力崇,長吁一口氣:“原來是場噩夢啊……” “哼斗塘!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起亮靴,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤馍盟,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后茧吊,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體贞岭,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年搓侄,在試婚紗的時候發(fā)現(xiàn)自己被綠了瞄桨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡讶踪,死狀恐怖芯侥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情俊柔,我是刑警寧澤筹麸,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站雏婶,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏白指。R本人自食惡果不足惜留晚,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望告嘲。 院中可真熱鬧错维,春花似錦、人聲如沸橄唬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽仰楚。三九已至隆判,卻和暖如春犬庇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背侨嘀。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工臭挽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人咬腕。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓欢峰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親涨共。 傳聞我的和親對象是個殘疾皇子纽帖,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容