java隊(duì)列Queue

在Java多線程應(yīng)用中,隊(duì)列的使用率很高,多數(shù)生產(chǎn)消費(fèi)模型的首選數(shù)據(jù)結(jié)構(gòu)就是隊(duì)列。Java提供的線程安全的Queue可以分為阻塞隊(duì)列和非阻塞隊(duì)列,其中阻塞隊(duì)列的典型例子是BlockingQueue,非阻塞隊(duì)列的典型例子是ConcurrentLinkedQueue,在實(shí)際應(yīng)用中要根據(jù)實(shí)際需要選用阻塞隊(duì)列或者非阻塞隊(duì)列。
注:什么叫線程安全?這個(gè)首先要明確。線程安全的類 ,指的是類內(nèi)共享的全局變量的訪問必須保證是不受多線程形式影響的。如果由于多線程的訪問(比如修改、遍歷、查看)而使這些變量結(jié)構(gòu)被破壞或者針對(duì)這些變量操作的原子性被破壞,則這個(gè)類就不是線程安全的。

BlockingQueue

BlockingQueue,顧名思義,“阻塞隊(duì)列”:可以提供阻塞功能的隊(duì)列。
首先,看看BlockingQueue提供的常用方法:

---- 可能報(bào)異常 返回布爾值 可能阻塞 設(shè)定等待時(shí)間
入隊(duì) add(e) offer(e) put(e) offer(e, timeout, unit)
出隊(duì) remove() poll() take() poll(timeout, unit)
查看 element() peek()
強(qiáng)調(diào)
  • add(e) remove() element() 方法不會(huì)阻塞線程。當(dāng)不滿足約束條件時(shí),會(huì)拋出IllegalStateException 異常。例如:當(dāng)隊(duì)列被元素填滿后,再調(diào)用add(e),則會(huì)拋出異常。
  • offer(e) poll() peek() 方法即不會(huì)阻塞線程,也不會(huì)拋出異常。例如:當(dāng)隊(duì)列被元素填滿后,再調(diào)用offer(e),則不會(huì)插入元素,函數(shù)返回false。
  • 要想要實(shí)現(xiàn)阻塞功能,需要調(diào)用put(e) take() 方法。當(dāng)不滿足約束條件時(shí),會(huì)阻塞線程。

以ArrayBlockingQueue類為例:

  • 第一類方法
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");//隊(duì)列已滿,拋異常
}

public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();//隊(duì)列為空,拋異常
}

  • 第二類方法
public boolean offer(E e) {
if (e == null)throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)//隊(duì)列已滿,返回false
return false;
else {
insert(e);//insert方法中發(fā)出了notEmpty.signal();
return true;
}
} finally {
lock.unlock();
}
}

public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == 0)//隊(duì)列為空,返回false
return null;
E x = extract();//extract方法中發(fā)出了notFull.signal();
return x;
} finally {
lock.unlock();
}
}
  • 第三類方法(這里面涉及到Condition類,簡(jiǎn)要提一下)
    await方法指:造成當(dāng)前線程在接到信號(hào)或被中斷之前一直處于等待狀態(tài)。
    signal方法指:?jiǎn)拘岩粋€(gè)等待線程。
public void put(E e)throws InterruptedException {
if (e == null)throw new NullPointerException();
final E[] items = this.items;
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
try {
while (count == items.length)//如果隊(duì)列已滿,等待notFull這個(gè)條件,這時(shí)當(dāng)前線程被阻塞
notFull.await();
} catch (InterruptedException ie) {
notFull.signal(); //喚醒受notFull阻塞的當(dāng)前線程
throw ie;
}
insert(e);
} finally {
lock.unlock();
}
}

public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
try {
while (count == 0)//如果隊(duì)列為空,等待notEmpty這個(gè)條件,這時(shí)當(dāng)前線程被阻塞
notEmpty.await();
} catch (InterruptedException ie) {
notEmpty.signal();//喚醒受notEmpty阻塞的當(dāng)前線程
throw ie;
}
E x = extract();
return x;
} finally {
lock.unlock();
}
}
  • 第四類方法就是指在有必要時(shí)等待指定時(shí)間,就不詳細(xì)說了。
BlockingQueue接口的具體實(shí)現(xiàn)類
  • ArrayBlockingQueue,其構(gòu)造函數(shù)必須帶一個(gè)int參數(shù)來指明其大小
  • LinkedBlockingQueue,若其構(gòu)造函數(shù)帶一個(gè)規(guī)定大小的參數(shù),生成的BlockingQueue有大小限制,若不帶大小參數(shù),所生成的BlockingQueue的大小由Integer.MAX_VALUE來決定
  • PriorityBlockingQueue,其所含對(duì)象的排序不是FIFO,而是依據(jù)對(duì)象的自然排序順序或者是構(gòu)造函數(shù)的Comparator決定的順序
上面是用ArrayBlockingQueue舉得例子,下面看看LinkedBlockingQueue:

首先,既然是鏈表,就應(yīng)該有Node節(jié)點(diǎn),它是一個(gè)內(nèi)部靜態(tài)類:

static class Node<E> {
/** The item, volatile to ensure barrier separating write and read */
volatile E item;
Node<E> next;
Node(E x) { item = x; }
}

然后,對(duì)于鏈表來說,肯定需要兩個(gè)變量來標(biāo)示頭和尾:

/** 頭指針 */
private transient Node<E> head;//head.next是隊(duì)列的頭元素
/** 尾指針 */
private transient Node<E> last;//last.next是null

那么,對(duì)于入隊(duì)和出隊(duì)就很自然能理解了:

private void enqueue(E x) {
last = last.next = new Node<E>(x);//入隊(duì)是為last再找個(gè)下家
}

private E dequeue() {
Node<E> first = head.next; //出隊(duì)是把head.next取出來,然后將head向后移一位
head = first;
E x = first.item;
first.item = null;
return x;
}

另外,LinkedBlockingQueue相對(duì)于ArrayBlockingQueue還有不同是,有兩個(gè)ReentrantLock,且隊(duì)列現(xiàn)有元素的大小由一個(gè)AtomicInteger對(duì)象標(biāo)示。

  • 注:AtomicInteger類是以原子的方式操作整型變量。
private final AtomicInteger count =new AtomicInteger(0);
/** 用于讀取的獨(dú)占鎖*/
private final ReentrantLock takeLock =new ReentrantLock();
/** 隊(duì)列是否為空的條件 */
private final Condition notEmpty = takeLock.newCondition();
/** 用于寫入的獨(dú)占鎖 */
private final ReentrantLock putLock =new ReentrantLock();
/** 隊(duì)列是否已滿的條件 */
private final Condition notFull = putLock.newCondition();

有兩個(gè)Condition很好理解,在ArrayBlockingQueue也是這樣做的。但是為什么需要兩個(gè)ReentrantLock呢?下面會(huì)慢慢道來。
讓我們來看看offer和poll方法的代碼:

public boolean offer(E e) {
if (e == null)throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity)
return false;
int c = -1;
final ReentrantLock putLock =this.putLock;//入隊(duì)當(dāng)然用putLock
putLock.lock();
try {
if (count.get() < capacity) {
enqueue(e); //入隊(duì)
c = count.getAndIncrement(); //隊(duì)長(zhǎng)度+1
if (c + 1 < capacity)
notFull.signal(); //隊(duì)列沒滿,當(dāng)然可以解鎖了
}
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();//這個(gè)方法里發(fā)出了notEmpty.signal();
return c >= 0;
}

public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0)
return null;
E x = null;
int c = -1;
final ReentrantLock takeLock =this.takeLock;出隊(duì)當(dāng)然用takeLock
takeLock.lock();
try {
if (count.get() > 0) {
x = dequeue();//出隊(duì)
c = count.getAndDecrement();//隊(duì)長(zhǎng)度-1
if (c > 1)
notEmpty.signal();//隊(duì)列沒空,解鎖
}
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();//這個(gè)方法里發(fā)出了notFull.signal();
return x;
}

看看源代碼發(fā)現(xiàn)和上面ArrayBlockingQueue的很類似,關(guān)鍵的問題在于:為什么要用兩個(gè)ReentrantLockputLock和takeLock?
我們仔細(xì)想一下,入隊(duì)操作其實(shí)操作的只有隊(duì)尾引用last,并且沒有牽涉到head。而出隊(duì)操作其實(shí)只針對(duì)head,和last沒有關(guān)系。那么就是說入隊(duì)和出隊(duì)的操作完全不需要公用一把鎖,所以就設(shè)計(jì)了兩個(gè)鎖,這樣就實(shí)現(xiàn)了多個(gè)不同任務(wù)的線程入隊(duì)的同時(shí)可以進(jìn)行出隊(duì)的操作,另一方面由于兩個(gè)操作所共同使用的count是AtomicInteger類型的,所以完全不用考慮計(jì)數(shù)器遞增遞減的問題。
另外,還有一點(diǎn)需要說明一下:await()和singal()這兩個(gè)方法執(zhí)行時(shí)都會(huì)檢查當(dāng)前線程是否是獨(dú)占鎖的當(dāng)前線程,如果不是則拋出java.lang.IllegalMonitorStateException異常。所以可以看到在源碼中這兩個(gè)方法都出現(xiàn)在Lock的保護(hù)塊中。

=============分隔================

下面再來說說ConcurrentLinkedQueue,它是一個(gè)無鎖的并發(fā)線程安全的隊(duì)列。
對(duì)比鎖機(jī)制的實(shí)現(xiàn),使用無鎖機(jī)制的難點(diǎn)在于要充分考慮線程間的協(xié)調(diào)。簡(jiǎn)單的說就是多個(gè)線程對(duì)內(nèi)部數(shù)據(jù)結(jié)構(gòu)進(jìn)行訪問時(shí),如果其中一個(gè)線程執(zhí)行的中途因?yàn)橐恍┰虺霈F(xiàn)故障,其他的線程能夠檢測(cè)并幫助完成剩下的操作。這就需要把對(duì)數(shù)據(jù)結(jié)構(gòu)的操作過程精細(xì)的劃分成多個(gè)狀態(tài)或階段,考慮每個(gè)階段或狀態(tài)多線程訪問會(huì)出現(xiàn)的情況。
ConcurrentLinkedQueue有兩個(gè)volatile的線程共享變量:head,tail。要保證這個(gè)隊(duì)列的線程安全就是保證對(duì)這兩個(gè)Node的引用的訪問(更新,查看)的原子性和可見性,由于volatile本身能夠保證可見性,所以就是對(duì)其修改的原子性要被保證。
下面通過offer方法的實(shí)現(xiàn)來看看在無鎖情況下如何保證原子性:

public boolean offer(E e) {
if (e == null)throw new NullPointerException();
Node<E> n = new Node<E>(e, null);
for (;;) {
Node<E> t = tail;
Node<E> s = t.getNext();
if (t == tail) { //------------------------------a
if (s == null) {//---------------------------b
if (t.casNext(s, n)) { //-------------------c
casTail(t, n); //------------------------d
return true;
}
} else {
casTail(t, s); //----------------------------e
}
}
}
}

此方法的循環(huán)內(nèi)首先獲得尾指針和其next指向的對(duì)象,由于tail和Node的next均是volatile的,所以保證了獲得的分別都是最新的值。
代碼a:t==tail是最上層的協(xié)調(diào),如果其他線程改變了tail的引用,則說明現(xiàn)在獲得不是最新的尾指針需要重新循環(huán)獲得最新的值。
代碼b:s==null的判斷。靜止?fàn)顟B(tài)下tail的next一定是指向null的,但是多線程下的另一個(gè)狀態(tài)就是中間態(tài):tail的指向沒有改變,但是其next已經(jīng)指向新的結(jié)點(diǎn),即完成tail引用改變前的狀態(tài),這時(shí)候s!=null。這里就是協(xié)調(diào)的典型應(yīng)用,直接進(jìn)入代碼e去協(xié)調(diào)參與中間態(tài)的線程去完成最后的更新,然后重新循環(huán)獲得新的tail開始自己的新一次的入隊(duì)嘗試。另外值得注意的是a,b之間,其他的線程可能會(huì)改變tail的指向,使得協(xié)調(diào)的操作失敗。從這個(gè)步驟可以看到無鎖實(shí)現(xiàn)的復(fù)雜性。
代碼c:t.casNext(s, n)是入隊(duì)的第一步,因?yàn)槿腙?duì)需要兩步:更新Node的next,改變tail的指向。代碼c之前可能發(fā)生tail引用指向的改變或者進(jìn)入更新的中間態(tài),這兩種情況均會(huì)使得t指向的元素的next屬性被原子的改變,不再指向null。這時(shí)代碼c操作失敗,重新進(jìn)入循環(huán)。
代碼d:這是完成更新的最后一步了,就是更新tail的指向,最有意思的協(xié)調(diào)在這兒又有了體現(xiàn)。從代碼看casTail(t, n)不管是否成功都會(huì)接著返回true標(biāo)志著更新的成功。首先如果成功則表明本線程完成了兩步的更新,返回true是理所當(dāng)然的;如果 casTail(t, n)不成功呢?要清楚的是完成代碼c則代表著更新進(jìn)入了中間態(tài),代碼d不成功則是tail的指向被其他線程改變。意味著對(duì)于其他的線程而言:它們得到的是中間態(tài)的更新,s!=null,進(jìn)入代碼e幫助本線程執(zhí)行最后一步并且先于本線程成功。這樣本線程雖然代碼d失敗了,但是是由于別的線程的協(xié)助先完成了,所以返回true也就理所當(dāng)然了。
通過分析這個(gè)入隊(duì)的操作,可以清晰的看到無鎖實(shí)現(xiàn)的每個(gè)步驟和狀態(tài)下多線程之間的協(xié)調(diào)和工作。
注:上面這大段文字看起來很累,先能看懂多少看懂多少,現(xiàn)在看不懂先不急,下面還會(huì)提到這個(gè)算法,并且用示意圖說明,就易懂很多了。

在使用ConcurrentLinkedQueue時(shí)要注意,如果直接使用它提供的函數(shù),比如add或者poll方法,這樣我們自己不需要做任何同步。
但如果是非原子操作,比如:

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

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

synchronized(queue) {
if(!queue.isEmpty()) {
queue.poll(obj);
}
}
  • 注:這種需要進(jìn)行自己同步的情況要視情況而定,不是任何情況下都需要這樣做。
    另外還說一下,ConcurrentLinkedQueue的size()是要遍歷一遍集合的,所以盡量要避免用size而改用isEmpty(),以免性能過慢。

[文章轉(zhuǎn)載自]madun大神

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,687評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,682評(píng)論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,011評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評(píng)論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,183評(píng)論 0 290
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,714評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,435評(píng)論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,665評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評(píng)論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,838評(píng)論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評(píng)論 1 295
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,379評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,627評(píng)論 2 380

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

  • 在java多線程應(yīng)用中,隊(duì)列的使用率很高,多數(shù)生產(chǎn)消費(fèi)模型的首選數(shù)據(jù)結(jié)構(gòu)就是隊(duì)列。Java提供的線程安全的Queu...
    騷的掉渣閱讀 5,350評(píng)論 2 4
  • 今天來介紹Java并發(fā)編程中最受歡迎的同步類——堪稱并發(fā)一枝花之BlockingQueue。 JDK版本:orac...
    猴子007閱讀 1,270評(píng)論 1 14
  • 第三章 Java內(nèi)存模型 3.1 Java內(nèi)存模型的基礎(chǔ) 通信在共享內(nèi)存的模型里,通過寫-讀內(nèi)存中的公共狀態(tài)進(jìn)行隱...
    澤毛閱讀 4,378評(píng)論 2 22
  • “你信仰的人說的每句話都是有分量的。”今天在書里看到這句話,然后我想起你。 如果一定要這么說,你大概就是...
    素心孤城閱讀 436評(píng)論 0 4
  • 今天在得到APP看了《前哨》專欄的王煜全的直播。王煜全口才了得,就口語表達(dá)這項(xiàng)我想在得到專欄中難逢敵手,思維...
    陌白Carl閱讀 310評(píng)論 3 0