摘要:在Java里開發(fā)多線程強有力的實踐就是做服務(wù)端的并發(fā)處理,本文作者闡述了實施多線程的三種實踐方法。要真的掌握某種技術(shù)你就必須要知其所以然。希望對Web開發(fā)者有所幫助。
【編者按】在Java里開發(fā)多線程強有力的實踐就是做服務(wù)端的并發(fā)處理,本文作者闡述了實施多線程的具體實踐方法,要真的掌握某種技術(shù)你就必須要知其所以然。筆者轉(zhuǎn)發(fā)至此,希望對Web開發(fā)者有所幫助。
全文如下:
作為一名Web工程師都希望自己做的Web應(yīng)用能被越來越多的人使用,如果我們所做的Web應(yīng)用隨著用戶的增多而宕機了,那么越來越多的人就會變得越來越少了,為了讓我們的Web應(yīng)用能有更多人使用,我們就得提升Web應(yīng)用服務(wù)端的并發(fā)能力。那么我們?nèi)绾巫龅竭@點了,根據(jù)現(xiàn)有的并發(fā)技術(shù)我們會有如下選擇:
給服務(wù)端請求開啟線程
個做法:為了每個客戶端發(fā)送給服務(wù)端的請求都開啟一個線程,等請求處理完畢后該線程就被銷毀掉,這種做法很直觀,但是在現(xiàn)代的Web服務(wù)器里這種做法已經(jīng)很少使用了,原因是新建一個線程,銷毀一個線程的開銷(開銷是指占用計算機系統(tǒng)資源例如:CPU、內(nèi)存等)是很大的,它時常會大于實際處理請求本身的開銷,因此這種方式不能充分利用計算機資源,提升并發(fā)的效率是有效的,要是還碰到線程安全的問題,使用到線程的鎖機制,數(shù)據(jù)同步技術(shù),并發(fā)提升就會受到更大的限制;除此之外,來一個請求就開啟一個線程,對線程數(shù)量沒有任何控制,這就會很容易導(dǎo)致計算機資源被用盡,對于Web服務(wù)端的穩(wěn)定性產(chǎn)生很大的威脅。
提高服務(wù)端并發(fā)量
第二個做法:鑒于上面的問題,我們就產(chǎn)生了第二種提高服務(wù)端并發(fā)量的方法,首先我們不再是一個客戶端請求過來就開啟一個新線程,請求處理完畢就銷毀線程,而是使用一種池技術(shù)即線程池技術(shù),線程池技術(shù)就是事先創(chuàng)建一批線程,這批線程被放入到一個池子里,在沒有請求到達服務(wù)端時候,這些線程都是處于待命狀態(tài),當(dāng)請求到達時候,程序會從線程池里取出一個線程,這個線程處理到達的請求,請求處理完畢,該線程不會被銷毀,而是被線程池回收,這種方式使用線程我們降低了隨意創(chuàng)建線程和銷毀線程所導(dǎo)致系統(tǒng)開銷,同時也控制了服務(wù)端線程的數(shù)量,一般一個線程對應(yīng)一個請求,也就控制了并發(fā)請求的個數(shù),該方案比種方案提升了系統(tǒng)的穩(wěn)定性(控制并發(fā)數(shù)量,防止并發(fā)過多導(dǎo)致服務(wù)程序宕機)同時也提升了并發(fā)的數(shù)量(原因是減少了創(chuàng)建線程和銷毀線程的開銷,更充分的利用了計算機的系統(tǒng)資源)。
但是做法二也是有很大的問題的,具體如下:做法二和做法一相比,做法二要好多了,但是這只是和做法一比,如果按照我們設(shè)計的目標(biāo),做法二并非完美,原因如下:首先做法二會讓很多技術(shù)不扎實人認(rèn)為線程池開啟多少線程就決定了系統(tǒng)并發(fā)的數(shù)量,因此出于讓系統(tǒng)能處理更多請求以及充分利用計算機資源的考慮,有些人會一開始就把線程池里新建線程的個數(shù)設(shè)置為大,一個Web應(yīng)用的并發(fā)量在一定時間里都是一個曲線形式,峰值在一定時間范圍內(nèi)都是少數(shù)情況,因此一開始就開啟大線程數(shù),自然在大多數(shù)時間內(nèi)都是在浪費系統(tǒng)資源,如果這些被浪費被閑置的計算資源能用來處理請求,或許這些請求處理的效率會更高。
此外,一個服務(wù)器到底預(yù)先開啟多少個線程,這個標(biāo)準(zhǔn)很難把控,還有就是不管你用線程池技術(shù)還是新建線程的方式,處理請求的數(shù)量和線程數(shù)量數(shù)量是一一對應(yīng)的關(guān)系,如果有一個時間點過來的請求數(shù)量正好超出了線程池里線程數(shù)量,例如就多了一個,那么這個請求因為找不到對應(yīng)線程很有可能會被程序所遺棄掉,其實這多的一個請求并沒有超出計算機所能承受的負載,而是因為我們程序設(shè)計不合理才被遺棄的,這肯定是開發(fā)人員所不愿意發(fā)生的事情。
JDK里的線程池對線程池大小的設(shè)定很關(guān)鍵
針對這些問題在Java的JDK里提供的線程池做了很好的解決(線程池技術(shù)是博大精深的,如果我們沒有研究透池技術(shù),還是不要自己去寫個而是用現(xiàn)成的),JDK里的線程池對線程池大小的設(shè)定使用兩個參數(shù),一個是核心線程個數(shù),一個是大線程個數(shù),核心線程在系統(tǒng)啟動時候就會被創(chuàng)建,如果用戶請求沒有超過核心線程處理能力,那么線程池不會再創(chuàng)建新線程,如果核心線程個數(shù)已經(jīng)處理不過來了,線程池就會開啟新線程,新線程次創(chuàng)建后,使用完畢后也不是立即對其銷毀,也是被會收到線程池里,當(dāng)線程池里的線程總數(shù)超過了大線程個數(shù),線程池將不會再創(chuàng)建新線程,這種做法讓線程數(shù)量根據(jù)實際請求的情況進行調(diào)整,這樣既達到了充分利用計算機資源的目的,同時也避免了系統(tǒng)資源的浪費。
JDK的線程池還有個超時時間,當(dāng)超出核心線程的線程在一定時間內(nèi)一直未被使用,那么這些線程將會被銷毀,資源就會被釋放,這樣就讓線程池的線程的數(shù)量總是處在一個合理的范圍里;如果請求實在太多了,線程池里的線程暫時處理不過來了,JDK的線程池還提供一個隊列機制,讓這些請求排隊等待,當(dāng)某個線程處理完畢,該線程又會從這個隊列里取出一個請求進行處理,這樣就避免請求的丟失,JDK的線程池對隊列的管理有很多策略,有興趣的童鞋可以問問度娘,這里我還要說的是JDK線程池的安全策略做的很好,如果隊列的容量超出了計算機的處理能力,隊列會拋棄無法處理的請求,這個也叫做線程池的拒絕策略。
看我這么詳細的描述做法二,是不是做法二就是一個完美的方案了?答案當(dāng)然是否定了,做法二并非高效的方案,做法二也沒有充分利用好計算機的系統(tǒng)資源,我這里還有做法三了,其具體做法如下:
首先我要提出一個問題,并發(fā)處理一個任務(wù)和單線程的處理同樣一個任務(wù),那種方式的效率更高?也許有很多人會認(rèn)為當(dāng)然是并發(fā)處理任務(wù)效率更高了,兩個人做一件事情總比一個人要厲害吧,這個問題的答案是要看場景的,在單核時代,單線程處理一個任務(wù)的效率往往會比并發(fā)方式效率更高,為什么呢?因為多線程在單核即單個CPU上運算,CPU并不是也可以并發(fā)處理的,CPU每次都只能處理一個計算任務(wù),因此并發(fā)任務(wù)對于CPU而言就有線程的上下文切換操作,而這種線程上下文的開銷是比較大的,因此單核上處理并發(fā)請求不一定會比單線程更有效率,但是如果到了多核的計算機,并發(fā)任務(wù)平均分配給每一個CPU,那么并發(fā)處理的效率就會比單線程處理要高很多,因為此時可以避免線程上下文的切換。
對于一個網(wǎng)絡(luò)請求的處理,是由兩個不同類型的操作共同完成,這兩個操作是CPU的計算操作和IO操作,如果我們以處理效率角度來評判這兩個操作,CPU操作效率是光速的,而IO操作就不盡然了,計算機里的IO操作就是對存儲數(shù)據(jù)介質(zhì)的操作,計算機里有如下幾個介質(zhì)可以存儲數(shù)據(jù),它們分別是:CPU的一級緩存、二級緩存、內(nèi)存、硬盤和網(wǎng)絡(luò),一級緩存存儲和讀取數(shù)據(jù)的能力接近光速,它比二級緩存快個5倍到6倍,但是不管是一級緩存還是二級緩存,它們存儲數(shù)據(jù)量太少了,做不了什么大事情,下面就是內(nèi)存了,以一級緩存的效率做參照,一級緩存比內(nèi)存速度快100多倍,到了硬盤存儲和讀取數(shù)據(jù)效率就更慢了,一級緩存比硬盤要快1000多萬倍,到了網(wǎng)絡(luò)就慢的更不像話了,一級緩存比網(wǎng)絡(luò)要快一億多倍,可見一個請求處理的效率瓶頸都是由IO引起的,而CPU雖然處理很快但是CPU對任務(wù)的計算都是一個接著一個處理,假如一個請求首先要等待網(wǎng)絡(luò)數(shù)據(jù)的處理在進行CPU運算,那么必然就拖慢了CPU的處理的整體效率,這一慢就是上億倍了,但是現(xiàn)實中一個網(wǎng)絡(luò)請求處理就是由這兩個操作組合而成的。
對于IO操作在Java里有兩種方式,一種方式叫做阻塞的IO,一種方式叫做非阻塞的IO,阻塞的IO就是在做IO操作時候,CPU要等待IO操作,這就造成了CPU計算資源的浪費,浪費的程度上文里已經(jīng)寫到了,是很可怕的,因此我們就想當(dāng)一個請求一個線程做IO操作時候,CPU不用等待它而是接著處理其他的線程和請求,這種做法效率必然很高,這時候非阻塞IO就登場了,非阻塞IO可以在線程進行IO操作時候讓CPU去處理別的線程,那么非阻塞IO怎么做到這一點的呢?非阻塞IO操作在請求和CPU計算之間添加了一個中間層,請求先發(fā)到這個中間層,中間層獲取了請求后就直接通知請求發(fā)送者,請求接收到了,注意這個時候中間層啥都沒干,只是接收了請求,真正的計算任務(wù)還沒開始哦,這個時候中間層如果要CPU處理那么就讓CPU處理,如果計算過程到了要進行IO操作,中間層就告訴CPU不用等我了,中間層就讓請求做IO操作,CPU這時候可以處理別的請求,等IO操作做完了,中間層再把任務(wù)交給CPU去處理,處理完成后,中間層將處理結(jié)果再發(fā)送給客戶端,這種方式就可以充分利用CPU的計算機資源,有了非阻塞IO其實使用單線程也可以開發(fā)多線程任務(wù),甚至這個單線程的處理效率可能比多線程更高,因為它沒有線程創(chuàng)建銷毀的開銷,也沒有線程上下文切換的開銷。
Node.js利用非阻塞的技術(shù)編寫更高效的Web服務(wù)器
其實實現(xiàn)一個非阻塞的請求是個大課題,里面使用到了很多先進和復(fù)雜的技術(shù)例如:回調(diào)函數(shù)和輪詢等,對于非阻塞的開發(fā)我目前掌握的還不夠好,等我有天完全掌握了它我一定會再寫一篇文章,不過這里要提到的是像Java里netty技術(shù),Nginx,PHP的并發(fā)處理都用到這種機制的原理,特別是現(xiàn)在很火的Node.js它產(chǎn)生的原因就是依靠這種非阻塞的技術(shù)來編寫更高效的Web服務(wù)器,可以說Node.js把這種技術(shù)用到了極致,不過這里要糾正下,非阻塞是針對IO操作的技術(shù),對于Node.js,netty的實現(xiàn)機制有更好的術(shù)語描述就是事件驅(qū)動(其實就是使用回調(diào)函數(shù),觀察者模式實現(xiàn)的)以及異步的IO技術(shù)(就是非阻塞的IO技術(shù))。
現(xiàn)在我們回到做法三的描述,做法三的核心思想就是讓每個線程資源利用率更加有效,做法三是建立在做法二的基礎(chǔ)上,使用事件驅(qū)動的開發(fā)思想,采用非阻塞的IO編程模式,當(dāng)客戶端多個請求發(fā)到服務(wù)端,服務(wù)端可以只用一個線程對這些請求進行處理,利用IO操作的性能瓶頸,充分利用CPU的計算能力,這樣就達到一個線程處理多個請求的效率并不比多線程差,甚至還高,同時單線程處理能力的增強也會導(dǎo)致整個Web服務(wù)并發(fā)性能的提升。大家可以想想,按這種方式在一個多核服務(wù)器下,假如這個服務(wù)器有8個內(nèi)核,每個內(nèi)核開啟一個線程,這8個線程也許就能承載數(shù)千并發(fā)量,同時也充分利用每個CPU計算能力,如果我們開啟線程越多(當(dāng)然新增的線程數(shù)好是8的倍數(shù),這樣對多核利用率更好)那么并發(fā)的效率也就更高,提升是按幾何倍數(shù)進行的,大家想想Nginx,它就采用此模式,所以它剛推出來的時候其并發(fā)處理能力是Apache服務(wù)器的數(shù)倍,現(xiàn)在Nginx已經(jīng)和Apache一樣普及了,事件驅(qū)動的異步機制功不可沒。
好了,文章寫畢,今天寫這篇文章算是對我近研究多線程的一點總結(jié),也是我近轉(zhuǎn)向研究Node.js的開始,Node.js有完美的異步編程模型,但是近我確一直懷疑它的并發(fā)能力,因為我一直沒找到Node.js里像Java里那么復(fù)雜的異步編程技術(shù),現(xiàn)在我發(fā)現(xiàn),Node.js用了一種更加巧妙的方式解決異步開發(fā)的問題,而且這種方式是高效,就這一點Node.js太有魅力了,所以很值得研究和學(xué)習(xí)。
原文鏈接:http://www.cnblogs.com/sharpxiajun/
本站文章版權(quán)歸原作者及原出處所有 。內(nèi)容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責(zé),本站只提供參考并不構(gòu)成任何投資及應(yīng)用建議。本站是一個個人學(xué)習(xí)交流的平臺,網(wǎng)站上部分文章為轉(zhuǎn)載,并不用于任何商業(yè)目的,我們已經(jīng)盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯(lián)系我們,我們將根據(jù)著作權(quán)人的要求,立即更正或者刪除有關(guān)內(nèi)容。本站擁有對此聲明的最終解釋權(quán)。