环形队列出队的元素怎么输出出来_队列:队列在线程池等有限资源池中的应用...
我們知道,CPU資源是有限的,任務(wù)的處理速度與線程個(gè)數(shù)并不是線性正相關(guān)的。相反,過多的線程反而會(huì)導(dǎo)致CPU頻繁切換,處理性能下降。所以,線程池的大小一般都是綜合考慮要處理任務(wù)的特點(diǎn)和硬件環(huán)境,來事先設(shè)置的。
當(dāng)我們向固定大小的線程池中請(qǐng)求一個(gè)線程時(shí),如果線程池沒有空閑資源了,這個(gè)時(shí)候線程池如何處理這個(gè)請(qǐng)求?是拒絕請(qǐng)求還是排隊(duì)請(qǐng)求?各種處理策略又是怎么實(shí)現(xiàn)的呢?
實(shí)際上,這些問題并不復(fù)雜,其底層的數(shù)據(jù)結(jié)構(gòu)就是我們今天要學(xué)的內(nèi)容,隊(duì)列(queue)。
如果理解“隊(duì)列”
隊(duì)列這個(gè)概念非常好理解。你可以把它想象成排隊(duì)買票,先來的先買,后來的人只能站末尾,不允許插隊(duì)。先進(jìn)來先出,這就是典型的“隊(duì)列”。
我們知道,棧只支持兩個(gè)基本操作:入棧push()和出棧pop()。隊(duì)列跟棧非常相似,支持的操作也很有限,最基本的操作也是兩個(gè):入隊(duì)enqueue(),放一個(gè)數(shù)據(jù)到隊(duì)列尾部;出列dequeue(),從隊(duì)列頭部取一個(gè)元素。
所以,隊(duì)列跟棧一樣,也是一種操作受限的線性表數(shù)據(jù)結(jié)構(gòu)。
隊(duì)列的概念很好理解,基本操作也很容易掌握。作為一種非常基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu),隊(duì)列的應(yīng)用也非常廣泛,特別是一些具有某些額外特性的隊(duì)列,比如循環(huán)隊(duì)列,阻塞隊(duì)列,并發(fā)隊(duì)列。他們?cè)诤芏嗥讓酉到y(tǒng),框架,中間件的開發(fā)中,起著關(guān)鍵性的作用。比如高性能隊(duì)列Disruptor,Linux環(huán)形緩存,都用到循環(huán)并發(fā)隊(duì)列;Java concurrent并發(fā)包利用ArrayBlockingQueue來實(shí)現(xiàn)公平鎖。
順序隊(duì)列和鏈?zhǔn)疥?duì)列
我們知道了,隊(duì)列和棧一樣,也是一種抽象的數(shù)據(jù)結(jié)構(gòu)。它具有先進(jìn)先出的特性,支持在隊(duì)尾插入元素,在對(duì)頭刪除元素,那究竟該如何實(shí)現(xiàn)一個(gè)隊(duì)列呢?
跟棧一樣,隊(duì)列可以用數(shù)組來實(shí)現(xiàn),也可以用鏈表來實(shí)現(xiàn)。用數(shù)組實(shí)現(xiàn)的棧叫順序棧,用鏈表實(shí)現(xiàn)的棧叫鏈?zhǔn)綏?。同?#xff0c;用數(shù)組實(shí)現(xiàn)的隊(duì)列叫作順序隊(duì)列,用鏈表實(shí)現(xiàn)的隊(duì)列叫作鏈?zhǔn)疥?duì)列。
我們先來看下基于數(shù)組的實(shí)現(xiàn)方法。我用java語言實(shí)現(xiàn)了一下,不過并不包含Java語言的高級(jí)語法,而且我做了比較詳細(xì)的注釋,你應(yīng)該可以看懂。
//用數(shù)組實(shí)現(xiàn)的隊(duì)列比起棧的數(shù)組實(shí)現(xiàn),隊(duì)列的數(shù)組實(shí)現(xiàn)稍微有點(diǎn)兒復(fù)雜,但是沒關(guān)系。我稍微解釋一下實(shí)現(xiàn)思路,你很容易就能明白了。
對(duì)于棧來說,我們只需要一個(gè)棧頂指針就可以了。但是隊(duì)列需要兩個(gè)指針:一個(gè)是head指針,指向?qū)︻^;一個(gè)是tail指針,指向隊(duì)尾。
你可以結(jié)合下面這幅圖來理解。當(dāng)a,b,c,d依次入隊(duì)之后,隊(duì)列中的head指針指向下標(biāo)為0的位置,tail指針指向下標(biāo)為4的位置。
當(dāng)我們調(diào)用兩次出隊(duì)操作之后,隊(duì)列中head指針指向下標(biāo)為2的位置,tail指針仍然指向下標(biāo)為4的位置。
你肯定已經(jīng)發(fā)現(xiàn)了,隨著不停地進(jìn)行入隊(duì),出隊(duì)操作,head和tail都會(huì)持續(xù)往后移動(dòng)。當(dāng)tail移動(dòng)到最右邊,即使數(shù)組中還有空閑空間,也無法繼續(xù)往隊(duì)列中添加數(shù)據(jù)了。這個(gè)問題該如何解決呢?
你是否還記得,在數(shù)組那一節(jié),我們也遇到過類似的問題,就是數(shù)組的刪除操作會(huì)導(dǎo)致數(shù)組中的數(shù)組不連續(xù)。你還記得我們當(dāng)時(shí)是怎么解決的嗎?對(duì)用數(shù)據(jù)搬移!但是,每次進(jìn)行出隊(duì)操作都相當(dāng)于刪除數(shù)組下標(biāo)為0的數(shù)據(jù),要搬移整個(gè)隊(duì)列中的數(shù)據(jù),這樣出隊(duì)操作的時(shí)間復(fù)雜度就會(huì)從原來的O(1)變成O(n)。能不能優(yōu)化一下呢?
實(shí)際上,我們?cè)诔鲫?duì)時(shí)可以不用搬移數(shù)據(jù)。如果沒有空閑空間了,我們只需要在入隊(duì)時(shí),再集中觸發(fā)一次數(shù)據(jù)的搬移操作。借助這個(gè)思想,出隊(duì)函數(shù)dequeue()保持不變,我們稍加改造一下函數(shù)enqueue()的實(shí)現(xiàn),就可以輕松解決剛才的問題了。
下面是具體的代碼:
//入隊(duì)操作,將item放入隊(duì)尾從代碼中我們看到,當(dāng)隊(duì)列的tail指針移動(dòng)到數(shù)組的最右邊后,如果有新的數(shù)據(jù)入隊(duì),我們可以將head到tail之間的數(shù)據(jù),整體搬移到數(shù)組中0到tail-head的位置。
這種實(shí)現(xiàn)思路中,出隊(duì)操作的時(shí)間復(fù)雜度仍然是O(1),但入隊(duì)操作的時(shí)間復(fù)雜度還是O(1)嗎?你可以用我們第3節(jié),第4節(jié)講的算法復(fù)雜度分析方法,自己試著分析一下。
接下來,我們?cè)賮砜聪禄阪湵淼年?duì)列實(shí)現(xiàn)方法。
基于鏈表的實(shí)現(xiàn),我們同樣需要兩個(gè)指針:head指針和tail指針。他們分別指向鏈表的第一個(gè)結(jié)點(diǎn)和最后一個(gè)結(jié)點(diǎn)。如圖所示,入隊(duì)是,tail->next=new_node,tail = tail->next;出隊(duì)時(shí),head = head->next。
循環(huán)隊(duì)列
我們剛才用數(shù)組來實(shí)現(xiàn)隊(duì)列的時(shí)候,在tail == n時(shí),會(huì)有數(shù)據(jù)搬移操作,這樣入隊(duì)操作性能就會(huì)收到影響。那有沒有辦法能夠避免數(shù)據(jù)搬移呢?我們來看看循環(huán)隊(duì)列的解決思路。
循環(huán)隊(duì)列,顧名思義,它長(zhǎng)得像一個(gè)環(huán)。原本數(shù)組是有頭有尾的,是一條直線?,F(xiàn)在我們把首尾相連,扳成了一個(gè)環(huán)。我畫了一張圖,你可以直觀地感受一下。
我們可以看到,圖中這個(gè)隊(duì)列的大小為8,當(dāng)前head=4,tail=7。當(dāng)有一個(gè)新的元素a入隊(duì)時(shí),我們放入下標(biāo)為7的位置。但這個(gè)時(shí)候,我們并不把tail更新為8,而是將其在環(huán)中后移一位,到下標(biāo)為0的位置。當(dāng)再有一個(gè)元素b入隊(duì)時(shí),我們將b放入下標(biāo)為0的位置,然后tail加1更新為1。所以,在a,b依次入隊(duì)之后,循環(huán)隊(duì)列中的元素就變成了下面的樣子:
通過這樣的方法,我們成功避免了數(shù)據(jù)搬移操作??雌饋聿浑y理解,但是循環(huán)隊(duì)列的代碼實(shí)現(xiàn)難度要比前面講的非循環(huán)隊(duì)列難多了。要想寫出沒有bug的循環(huán)隊(duì)列的實(shí)現(xiàn)代碼,我個(gè)人覺得,最關(guān)鍵的是,確定好對(duì)空和隊(duì)滿的判定條件。
在用數(shù)組實(shí)現(xiàn)的非循環(huán)隊(duì)列中,隊(duì)滿的判斷條件是tail ==n,隊(duì)空的判斷條件是head==tail。那針對(duì)循環(huán)隊(duì)列,如何判斷隊(duì)空和隊(duì)滿呢?
隊(duì)列為空的判斷條件仍然是head == tail。但隊(duì)列滿的判斷條件就稍微有點(diǎn)復(fù)雜了。我畫了一張隊(duì)列滿的圖,你可以看一下,試著總結(jié)一下規(guī)律。
就像我圖中畫的隊(duì)滿的情況,tail=3,head=4,n=8,所以總結(jié)一下規(guī)律就是:(3+1)%8=4。
多畫幾張隊(duì)滿 的圖,你就會(huì)發(fā)現(xiàn),當(dāng)隊(duì)滿時(shí),(tail+1)%n=head。
你有沒有發(fā)現(xiàn),當(dāng)隊(duì)列滿時(shí),圖中的tail指向的位置實(shí)際上是沒有存儲(chǔ)數(shù)據(jù)的。所以,循環(huán)隊(duì)列會(huì)浪費(fèi)一個(gè)數(shù)組的存儲(chǔ)空間。
Taik is cheap,如果還是沒怎么理解,那就show 有code吧。
public阻塞隊(duì)列和并發(fā)隊(duì)列
前面講的內(nèi)容理論比較多,看起來很難跟實(shí)際的項(xiàng)目開發(fā)扯上關(guān)系。確實(shí),隊(duì)列這種數(shù)據(jù)結(jié)構(gòu)很基礎(chǔ),平時(shí)的業(yè)務(wù)開發(fā)不大可能從零實(shí)現(xiàn)一個(gè)隊(duì)列,甚至都不會(huì)直接用到。而一些具有特殊特性的隊(duì)列應(yīng)用卻比較廣泛,比如阻塞隊(duì)列和并發(fā)隊(duì)列。
阻塞隊(duì)列其實(shí)就是在隊(duì)列基礎(chǔ)上增加了阻塞操作。簡(jiǎn)單來說,就是在隊(duì)列為空的時(shí)候,從隊(duì)頭取數(shù)據(jù)會(huì)被阻塞。因?yàn)榇藭r(shí)還沒有數(shù)據(jù)可取,直到隊(duì)列中有了數(shù)據(jù)才能返回;如果隊(duì)列已經(jīng)滿了,那么插入數(shù)據(jù)的操作就會(huì)阻塞,直到隊(duì)列中有空閑位置后再插入數(shù)據(jù),然后再返回。
你應(yīng)該已經(jīng)發(fā)現(xiàn)了,上述的定義就是一個(gè)“生產(chǎn)者-消費(fèi)者模型!”是的,我們可以使用阻塞隊(duì)列,輕松實(shí)現(xiàn)一個(gè)“生產(chǎn)者-消費(fèi)者模型”!
這種基于阻塞隊(duì)列實(shí)現(xiàn)的“生產(chǎn)者-消費(fèi)者模型”,可以有效地協(xié)調(diào)生產(chǎn)和消費(fèi)的速度。當(dāng)“生產(chǎn)者”生產(chǎn)數(shù)據(jù)的速度過快,“消費(fèi)者”來不及消費(fèi)時(shí),存儲(chǔ)數(shù)據(jù)的隊(duì)列很快就會(huì)滿了。這個(gè)時(shí)候,生產(chǎn)者阻塞等待,知道“消費(fèi)者”消費(fèi)了數(shù)據(jù),“生產(chǎn)者”才會(huì)被喚醒繼續(xù)“生產(chǎn)”。
而且不僅如此,基于阻塞隊(duì)列,我們還可以通過協(xié)調(diào)“生產(chǎn)者”和“消費(fèi)者”的個(gè)數(shù),來提高數(shù)據(jù)的處理效率。比如前面的例子,我們可以配置幾個(gè)“消費(fèi)者”,來應(yīng)對(duì)一個(gè)“生產(chǎn)者”。
前面我們講了阻塞隊(duì)列,在多線程情況下,會(huì)有多個(gè)線程同時(shí)操作隊(duì)列,這個(gè)時(shí)候就會(huì)存在線程安全問題,那如何實(shí)現(xiàn)一個(gè)線程安全的隊(duì)列呢?
線程安全的隊(duì)列我們叫作并發(fā)隊(duì)列。最簡(jiǎn)單直接的實(shí)現(xiàn)方式是直接在enqueue(),dequeue()方法上加鎖,但是鎖粒度大并發(fā)度會(huì)比較低,同一時(shí)刻僅允許一個(gè)存或者取操作。實(shí)際上,基于數(shù)組的循環(huán)隊(duì)列,利用CAS原子操作,可以實(shí)現(xiàn)非常高效的并發(fā)隊(duì)列。這個(gè)也是循環(huán)隊(duì)列比鏈?zhǔn)疥?duì)列應(yīng)用更加廣泛的原因。在實(shí)戰(zhàn)篇講Disruptor的時(shí)候,我會(huì)再詳細(xì)講并發(fā)隊(duì)列的應(yīng)用。
解答開篇
隊(duì)列的知識(shí)就講完了,我們現(xiàn)在回過來看下開篇的問題。線程池沒有空閑線程時(shí),新的任務(wù)請(qǐng)求線程資源時(shí),線程池該如何處理?各種處理策略又是如何現(xiàn)實(shí)的呢?
我們一般有兩種處理策略。第一種是非阻塞的處理方式,直接拒絕任務(wù)請(qǐng)求;另一種是阻塞的處理方法,將請(qǐng)求排隊(duì),等到有空閑線程時(shí),取出排隊(duì)的請(qǐng)求繼續(xù)處理。那如何存儲(chǔ)排隊(duì)的請(qǐng)求呢?
我們希望公平地處理每個(gè)排隊(duì)的請(qǐng)求,先進(jìn)者先服務(wù),所以隊(duì)列這種數(shù)據(jù)結(jié)構(gòu)很適合來存儲(chǔ)排隊(duì)請(qǐng)求。我們前面說過,隊(duì)列有基于鏈表和基于數(shù)組這兩種實(shí)現(xiàn)方式。這兩種實(shí)現(xiàn)方式對(duì)于排隊(duì)請(qǐng)求又有什么區(qū)別呢?
基于鏈表的實(shí)現(xiàn)方式,可以實(shí)現(xiàn)一個(gè)支持無限排隊(duì)的無界隊(duì)列(unbounded queue),但是可能會(huì)導(dǎo)致過多的請(qǐng)求排隊(duì)等待,請(qǐng)求處理的響應(yīng)時(shí)間過長(zhǎng)。所以,針對(duì)響應(yīng)時(shí)間比較敏感的系統(tǒng),基于鏈表實(shí)現(xiàn)的無限排隊(duì)的線程池是不合適的。
而基于數(shù)組實(shí)現(xiàn)的有界隊(duì)列(bounded queue),隊(duì)列的大小有限,所以線程池中隊(duì)列的請(qǐng)求超過隊(duì)列大小時(shí),接下來的請(qǐng)求就會(huì)被拒絕,這種方式對(duì)響應(yīng)敏感的系統(tǒng)來說,就相對(duì)更加合理。不過,設(shè)置一個(gè)合理的隊(duì)列大小,也是非常有講究的。隊(duì)列太大導(dǎo)致等待的請(qǐng)求太多,隊(duì)列太小會(huì)導(dǎo)致無法充分利用系統(tǒng)資源,發(fā)揮最大性能。
除了前面講到隊(duì)列應(yīng)用在線程池請(qǐng)求隊(duì)列的場(chǎng)景之外,隊(duì)列可以應(yīng)用在任何有限資源池中,用于排隊(duì)請(qǐng)求,比如數(shù)據(jù)庫連接池等。實(shí)際上,對(duì)于大部分資源有限的場(chǎng)景,當(dāng)沒有空閑資源時(shí),基本上都可以通過“隊(duì)列”這種數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)請(qǐng)求排隊(duì)。
內(nèi)容小結(jié)
今天我們講 了一種跟棧很相似的數(shù)據(jù)結(jié)構(gòu),隊(duì)列。關(guān)于隊(duì)列,你能掌握下面的內(nèi)容,這節(jié)就沒問題了。
隊(duì)列最大的特點(diǎn)就是先進(jìn)先出,主要的兩個(gè)操作是入隊(duì)和出隊(duì)。跟棧一樣,它既可以用數(shù)組來實(shí)現(xiàn),也可以用鏈表來實(shí)現(xiàn)。用數(shù)組實(shí)現(xiàn)的叫順序隊(duì)列,用鏈表實(shí)現(xiàn)的叫鏈?zhǔn)疥?duì)列。特別是長(zhǎng)得像一個(gè)環(huán)的循環(huán)隊(duì)列。在數(shù)組實(shí)現(xiàn)隊(duì)列的時(shí)候,會(huì)有數(shù)組搬移操作,要想解決數(shù)據(jù)搬移的問題,我們就需要像環(huán)一樣的循環(huán)隊(duì)列。
循環(huán)隊(duì)列是我們這節(jié)的重點(diǎn)。要想寫出沒有bug的循環(huán)隊(duì)列實(shí)現(xiàn)代碼,關(guān)鍵要確定好隊(duì)空和隊(duì)滿的判定條件,具體的代碼你要能寫出來。
除此之外,我們還講了幾種高級(jí)隊(duì)列結(jié)構(gòu),阻塞隊(duì)列,并發(fā)隊(duì)列,底層都還是隊(duì)列這種數(shù)據(jù)結(jié)構(gòu),只不過在之上附加了很多其他功能。阻塞隊(duì)列就是入隊(duì),出隊(duì)操作可以阻塞,并發(fā)隊(duì)列就是隊(duì)列的操作多線程安全。
參考文獻(xiàn)
王爭(zhēng)老師 《數(shù)據(jù)結(jié)構(gòu)與算法之美》
總結(jié)
以上是生活随笔為你收集整理的环形队列出队的元素怎么输出出来_队列:队列在线程池等有限资源池中的应用...的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 友定存是什么?友定存收益怎么算?
- 下一篇: arduino代码_arduino智能小