文章出處

 

  最近我研究技術的一個重點是java的多線程開發,在我早期學習java的時候,很多書上把java的多線程開發標榜為簡單易用,這個簡單易用是以C語言作為參照的,不過我也沒有使用過C語言開發過多線程,我只知道我學習java多線程開發是很難的,直到現在寫這篇文章的時候,雖然我對java多線程里的API比以前熟悉更多了,但是如果碰到了生產開發里如何將多線程設計更好,我心里的底氣還是不足的,哎,缺乏很有意義的實踐,我現在要等待讓我實踐這部分技術的機會了。

  話外話,研究多線程是因為我在一本講并發編程的書籍里看到書里作者把能做好并發編程的工程師叫做并發工程師,這和我研究web前端技術時候看到前端工程師的感受類似,因此我想找機會也把自己訓練成為一名并發工程師。

  廢話少說,回到本文的主題,作為一名web工程師都希望自己做的web應用能被越來越多的人使用,如果我們所做的web應用隨著用戶的增多而宕機了,那么越來越多的人就會變得越來越少了,為了讓我們的web應用能有更多人使用,我們就得提升web應用服務端的并發能力。那么我們如何做到這點了,根據現有的并發技術我們會有如下選擇:

  第一個做法:為了每個客戶端發送給服務端的請求都開啟一個線程,等請求處理完畢后該線程就被銷毀掉,這種做法很直觀,但是在現代的web服務器里這種做法已經很少使用了,原因是新建一個線程,銷毀一個線程的開銷(開銷是指占用計算機系統資源例如:cpu、內存等)是很大的,它時常會大于實際處理請求本身的開銷,因此這種方式不能充分利用計算機資源,提升并發的效率是有效的,要是還碰到線程安全的問題,使用到線程的鎖機制,數據同步技術,并發提升就會受到更大的限制;除此之外,來一個請求就開啟一個線程,對線程數量沒有任何控制,這就會很容易導致計算機資源被用盡,對于web服務端的穩定性產生很大的威脅。

  第二個做法:鑒于上面的問題,我們就產生了第二種提高服務端并發量的方法,首先我們不再是一個客戶端請求過來就開啟一個新線程,請求處理完畢就銷毀線程,而是使用一種池技術即線程池技術,線程池技術就是事先創建一批線程,這批線程被放入到一個池子里,在沒有請求到達服務端時候,這些線程都是處于待命狀態,當請求到達時候,程序會從線程池里取出一個線程,這個線程處理到達的請求,請求處理完畢,該線程不會被銷毀,而是被線程池回收,這種方式使用線程我們降低了隨意創建線程和銷毀線程所導致系統開銷,同時也控制了服務端線程的數量,一般一個線程對應一個請求,也就控制了并發請求的個數,該方案比第一種方案提升了系統的穩定性(控制并發數量,防止并發過多導致服務程序宕機)同時也提升了并發的數量(原因是減少了創建線程和銷毀線程的開銷,更充分的利用了計算機的系統資源)。但是做法二也是有很大的問題的,具體如下:

  做法二和做法一相比,做法二要好多了,但是這只是和做法一比,如果按照我們設計的目標,做法二并非完美,原因如下:首先做法二會讓很多技術不扎實人認為線程池開啟多少線程就決定了系統并發的數量,因此出于讓系統能處理更多請求以及充分利用計算機資源的考慮,有些人會一開始就把線程池里新建線程的個數設置為最大,一個web應用的并發量在一定時間里都是一個曲線形式,峰值在一定時間范圍內都是少數情況,因此一開始就開啟最大線程數,自然在大多數時間內都是在浪費系統資源,如果這些被浪費被閑置的計算資源能用來處理請求,或許這些請求處理的效率會更高。此外,一個服務器到底預先開啟多少個線程,這個標準很難把控,還有就是不管你用線程池技術還是新建線程的方式,處理請求的數量和線程數量數量是一一對應的關系,如果有一個時間點過來的請求數量正好超出了線程池里線程數量,例如就多了一個,那么這個請求因為找不到對應線程很有可能會被程序所遺棄掉,其實這多的一個請求并沒有超出計算機所能承受的負載,而是因為我們程序設計不合理才被遺棄的,這肯定是開發人員所不愿意發生的事情,針對這些問題在java的JDK里提供的線程池做了很好的解決(線程池技術是博大精深的,如果我們沒有研究透池技術,還是不要自己去寫個而是用現成的),jdk里的線程池對線程池大小的設定使用兩個參數,一個是核心線程個數,一個是最大線程個數,核心線程在系統啟動時候就會被創建,如果用戶請求沒有超過核心線程處理能力,那么線程池不會再創建新線程,如果核心線程個數已經處理不過來了,線程池就會開啟新線程,新線程第一次創建后,使用完畢后也不是立即對其銷毀,也是被會收到線程池里,當線程池里的線程總數超過了最大線程個數,線程池將不會再創建新線程,這種做法讓線程數量根據實際請求的情況進行調整,這樣既達到了充分利用計算機資源的目的,同時也避免了系統資源的浪費,jdk的線程池還有個超時時間,當超出核心線程的線程在一定時間內一直未被使用,那么這些線程將會被銷毀,資源就會被釋放,這樣就讓線程池的線程的數量總是處在一個合理的范圍里;如果請求實在太多了,線程池里的線程暫時處理不過來了,jdk的線程池還提供一個隊列機制,讓這些請求排隊等待,當某個線程處理完畢,該線程又會從這個隊列里取出一個請求進行處理,這樣就避免請求的丟失,jdk的線程池對隊列的管理有很多策略,有興趣的童鞋可以問問度娘,這里我還要說的是jdk線程池的安全策略做的很好,如果隊列的容量超出了計算機的處理能力,隊列會拋棄無法處理的請求,這個也叫做線程池的拒絕策略。

  看我這么詳細的描述做法二,是不是做法二就是一個完美的方案了?答案當然是否定了,做法二并非最高效的方案,做法二也沒有充分利用好計算機的系統資源,我這里還有做法三了,其具體做法如下:

  首先我要提出一個問題,并發處理一個任務和單線程的處理同樣一個任務,那種方式的效率更高?也許有很多人會認為當然是并發處理任務效率更高了,兩個人做一件事情總比一個人要厲害吧,這個問題的答案是要看場景的,在單核時代,單線程處理一個任務的效率往往會比并發方式效率更高,為什么呢?因為多線程在單核即單個cpu上運算,cpu并不是也可以并發處理的,cpu每次都只能處理一個計算任務,因此并發任務對于cpu而言就有線程的上下文切換操作,而這種線程上下文的開銷是比較大的,因此單核上處理并發請求不一定會比單線程更有效率,但是如果到了多核的計算機,并發任務平均分配給每一個cpu,那么并發處理的效率就會比單線程處理要高很多,因為此時可以避免線程上下文的切換。

  對于一個網絡請求的處理,是由兩個不同類型的操作共同完成,這兩個操作是CPU的計算操作和IO操作,如果我們以處理效率角度來評判這兩個操作,CPU操作效率是光速的,而IO操作就不盡然了,計算機里的IO操作就是對存儲數據介質的操作,計算機里有如下幾個介質可以存儲數據,它們分別是:CPU的一級緩存、二級緩存、內存、硬盤和網絡,一級緩存存儲和讀取數據的能力接近光速,它比二級緩存快個5倍到6倍,但是不管是一級緩存還是二級緩存,它們存儲數據量太少了,做不了什么大事情,下面就是內存了,以一級緩存的效率做參照,一級緩存比內存速度快100多倍,到了硬盤存儲和讀取數據效率就更慢了,一級緩存比硬盤要快1000多萬倍,到了網絡就慢的更不像話了,一級緩存比網絡要快一億多倍,可見一個請求處理的效率瓶頸都是由IO引起的,而CPU雖然處理很快但是CPU對任務的計算都是一個接著一個處理,假如一個請求首先要等待網絡數據的處理在進行CPU運算,那么必然就拖慢了CPU的處理的整體效率,這一慢就是上億倍了,但是現實中一個網絡請求處理就是由這兩個操作組合而成的。對于IO操作在java里有兩種方式,一種方式叫做阻塞的IO,一種方式叫做非阻塞的IO,阻塞的IO就是在做IO操作時候,CPU要等待IO操作,這就造成了CPU計算資源的浪費,浪費的程度上文里已經寫到了,是很可怕的,因此我們就想當一個請求一個線程做IO操作時候,CPU不用等待它而是接著處理其他的線程和請求,這種做法效率必然很高,這時候非阻塞IO就登場了,非阻塞IO可以在線程進行IO操作時候讓CPU去處理別的線程,那么非阻塞IO怎么做到這一點的呢?非阻塞IO操作在請求和cpu計算之間添加了一個中間層,請求先發到這個中間層,中間層獲取了請求后就直接通知請求發送者,請求接收到了,注意這個時候中間層啥都沒干,只是接收了請求,真正的計算任務還沒開始哦,這個時候中間層如果要CPU處理那么就讓cpu處理,如果計算過程到了要進行IO操作,中間層就告訴cpu不用等我了,中間層就讓請求做IO操作,CPU這時候可以處理別的請求,等IO操作做完了,中間層再把任務交給CPU去處理,處理完成后,中間層將處理結果再發送給客戶端,這種方式就可以充分利用CPU的計算機資源,有了非阻塞IO其實使用單線程也可以開發多線程任務,甚至這個單線程的處理效率可能比多線程更高,因為它沒有線程創建銷毀的開銷,也沒有線程上下文切換的開銷。其實實現一個非阻塞的請求是個大課題,里面使用到了很多先進和復雜的技術例如:回調函數和輪詢等,對于非阻塞的開發我目前掌握的還不夠好,等我有天完全掌握了它我一定會再寫一篇文章,不過這里要提到的是像java里netty技術,nginx,php的并發處理都用到這種機制的原理,特別是現在很火的nodejs它產生的原因就是依靠這種非阻塞的技術來編寫更高效的web服務器,可以說nodejs把這種技術用到了極致,不過這里要糾正下,非阻塞是針對IO操作的技術,對于nodejs,netty的實現機制有更好的術語描述就是事件驅動(其實就是使用回調函數,觀察者模式實現的)以及異步的IO技術(就是非阻塞的IO技術)。現在我們回到做法三的描述,做法三的核心思想就是讓每個線程資源利用率更加有效,做法三是建立在做法二的基礎上,使用事件驅動的開發思想,采用非阻塞的IO編程模式,當客戶端多個請求發到服務端,服務端可以只用一個線程對這些請求進行處理,利用IO操作的性能瓶頸,充分利用CPU的計算能力,這樣就達到一個線程處理多個請求的效率并不比多線程差,甚至還高,同時單線程處理能力的增強也會導致整個web服務并發性能的提升。大家可以想想,按這種方式在一個多核服務器下,假如這個服務器有8個內核,每個內核開啟一個線程,這8個線程也許就能承載數千并發量,同時也充分利用每個CPU計算能力,如果我們開啟線程越多(當然新增的線程數最好是8的倍數,這樣對多核利用率更好)那么并發的效率也就更高,提升是按幾何倍數進行的,大家想想nginx,它就采用此模式,所以它剛推出來的時候其并發處理能力是apache服務器的數倍,現在nginx已經和apache一樣普及了,事件驅動的異步機制功不可沒。

  好了,文章寫畢,今天寫這篇文章算是對我最近研究多線程的一點總結,也是我最近轉向研究nodejs的開始,nodejs有完美的異步編程模型,但是最近我確一直懷疑它的并發能力,因為我一直沒找到nodejs里像java里那么復雜的異步編程技術,現在我發現,nodejs用了一種更加巧妙的方式解決異步開發的問題,而且這種方式是高效,就這一點nodejs太有魅力了,所以很值得研究和學習。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()