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 有以下特點:
- 有界數(shù)組灵临,隊列容量在構(gòu)造時傳入截型;
- 通過 takeIndex / putIndex 設(shè)計,數(shù)組是 循環(huán)數(shù)組儒溉;
- lock 保護臨界區(qū)宦焦,notEmpty 和 notFull 用來阻塞和通知等待線程;
- 不接受 null 元素顿涣;
- 構(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 有以下特點:
- 內(nèi)部鏈表結(jié)構(gòu)庄撮,默認無界;
- 初始化時余黎,head 和 last 指向同一個占位 node重窟,該 node 的元素為 null;
- takeLock 保護多線程同步訪問 head惧财,取數(shù)據(jù)巡扇;
- putLock 保護多線程同步訪問 last,往隊列加數(shù)據(jù)垮衷;
- 讀線程和寫線程同步分離厅翔,提高隊列的并發(fā)行,同時可以有一個讀線程和寫線程訪問隊列搀突;
- 沒有公平性設(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 有以下特點:
- 內(nèi)部數(shù)組存儲數(shù)據(jù),讀和寫需要同一個鎖進行同步保護修械;
- 雖然是數(shù)組存儲趾牧,但是使用極小堆進行排序;
- 初始化可以指定 comparator肯污,如果沒有指定翘单,則需要對象實現(xiàn) Comparable 接口,調(diào)用它的 compareTo;
- 不接受 null
- 雖然是數(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 有以下特點:
- 沒有數(shù)據(jù)緩沖凉翻,寫線程和讀線程一一對應(yīng)了讨;
- 排隊等候的線程,而非數(shù)據(jù);
- 支持 FIFO 或 LIFO前计,公平模式下胞谭,使用 TransferQueue 支持 FIFO,非公平模式下男杈,使用 TransferStack 支持 LIFO韭赘;
- 內(nèi)部用鏈表存儲等候線程;
- 實現(xiàn)時势就,使用輪詢 CAS 來實現(xiàn)無鎖化處理泉瞻;
內(nèi)容來源
Java 并發(fā)編程實戰(zhàn)
http://ifeve.com/java-copy-on-write/
http://wsmajunfeng.iteye.com/blog/1629354