天天看點

Java的NIO以及線程并發

一、nio的出現

  nio是jdk1.4裡面才出現的東東,他給大家帶來的最大好處是異步socket。其它file,pipe暫時就不多談了。

  在jdk1.4出現之前,如果你需要編寫一個java伺服器,為了實作異步操作,你必須為每個連接配接請求生成一個java線程,當連接配接請求很多時,線程的排程,上下文切換,所付出的代價是非常昂貴,而且由于java是跨平台的,各個平台對線程的支援并不相同,性能也不相同,是以傳統的java伺服器程式設計架構是低效的且代價貴,dl大俠寫了個util.concurrent包後,總算是減輕了線程排程給java程式員帶來的痛苦,但是相比之與c、c++寫出來的伺服器,java伺服器在性能要求很高的情況下,基本上沒有什麼競争力,甚至是入圍的權利的都沒有。

  二、異步socket的實作

  nio出現後,好像讓java的程式員有了楊眉吐氣的機會,怎麼個吐氣法,當時大家是個什麼感受,俺是不知道,因為當時俺不搞java,對java的認識有限。

  nio是一個基于事件的io架構,最基本的思想就是:有事件我通知你,你再去做你的事情,沒事件時你大可以節約大把時間去做其它任何事情。而且nio的主線程only one,不像傳統的模型,需要n個線程去,也減輕了jvm的工作量,使得jvm處理任務時顯得更加高效。

  剛開始接觸nio時,被n層的channel架構、網上鋪天蓋地的好評給鎮住了,想想也應當是個很成熟的産品了,網上資料這麼多,抄一抄jetty、tomcat以及其它一些牛b的源代碼,基本上就能搞定了,此時沒有想到大家受同步的影響這麼深,也沒有想到連最基本的異步概念都沒有搞清楚就去寫代碼,搞出一堆的問題來(這是後話,後面再說)。

  現在研究了nio以後,發現nio實際上在java中做的工作是很簡單,就是将事件進行收集和分發,我們結合一個經典的調用例子來說明這個問題,我就不從nio的基本使用說起了,大家可以查其它的資料,網上一大把。

  當channel注冊至selector以後,我們的最經典的調用方法,是這樣子的。

while(somecondition)

{

}

  這隻是個小例子啊,什麼異常我就懶得抓了。

  nio中取得事件通知,就是在selector的select事件中完成的,在selector事件時有一個線程,這個線程具體的處理簡單點說就是:向作業系統詢問,selector中注冊的channel&&selectionkey的偶對各種事件是否有發生,如果有則添加到selector的selectedkeys屬性set中去,并傳回本次有多少個感興趣的事情發生。程式員發現這個值>0,表示有事件發生,馬上疊代selectedkeys中的selectionkey,根據key中的表示的事件,來做相應的處理。

 實際上,這段說明表明了異步socket的核心,即異步socket不過是将多個socket的排程(或者還有他們的線程排程)全部交給作業系統自己去完成,異步的核心selector,不過是将這些排程收集、分發而已。因為作業系統的socket、線程排程再咋d也比你jvm中要強,效率也高。

  而且就算jvm做的和作業系統一樣好,性能一樣高(當然這是不現實的),使用異步socket你至少也節約了一半的系統消耗,想想假定作業系統本身也是使用線程來維護n個socket連接配接,在傳統的java程式設計中,你還必須為這些socket還多起一個java線程,那至少是2n個線程,現在隻需要n+1。在高并發的情況下,你自己去想吧。

  懂了這個道理,異步socket也就好寫了,也不會搞得思路混亂了。

  三、異步socket中應當注意的事情

  1、讀

  異步socket最基本的理念就是事件通知,前面也說了,有事件通知你了,你才該做你應當做的事情。在異步socket中當注冊了一個op_read事件後,你就等着selector通知你吧,如果沒有通知你,你在家睡大覺都行。

  在這裡,我們有人出現的錯誤就是受同步的影響,自己去主動讀,而且還搞出了多線程,如果仔細考慮一下,就不會出現這個問題了。同步socket中,調用read方法讀取io中的資料時,通常情況下如果沒有資料read方法會阻塞,且是同步的,是以當多個線程同時通路時,read方法是線程安全的。

  而在異步下就不同,異步是不會阻塞的,有什麼就傳回什麼,你主動去讀,隻要有資料,你就可以拿走,在多線程的情況下,也許你是想讓第一個線程讀取,but此時來資料時正好是線程2讀到了,那線程2就高高興興的拿去,而線程1還在苦苦等待,這樣導緻資料混亂不說,如果後面再也不來資料了,線程1就是死循環啦。

  2、寫

  在異步socket中,寫是唯一一個主動點的操作,但是也不能直接去寫channel,而是應當先把自身注冊為op_writable,這時selector就會發現你的存在,并把給發一個write事件,你這時後就可以寫了,不過這時候有個小小的技巧,就是你執行寫操作之前,請取消掉你的寫注冊,否則你的cpu肯定是100%。

  3、等待

  在傳統的伺服器程式設計中,由于對于每個請求都是産生的一個線程,是以你在你每個請求線程中wait也好,sleep也好,不會影響别人。但是異步不同,他的主線程隻有一個,基本上每個處理都是線性的,也就是說處理完第一個,然後才能處理第二個,是以nio是一個極好的處理短連接配接的架構。

  我們現在出現的問題是,有人受同步的影響,沒有搞清異步是如何處理,竟然在方法進行中用上sleep,而且一等還是3秒,這意味着什麼,3秒才能處理一個請求,my god,我要一個3秒才能處理一個請求的伺服器幹嘛啊,還是60年代啊

  如果出現這樣的需要等待的情況,應當另起一個線程(推薦使用線程池)去完成這個“長”時間的任務,或者将其它交給一個消息隊列,通過發消息的方式将給别人去完成也行,用戶端能等,你伺服器怎麼也能等呢?寫出這樣的代碼,基本上一個伺服器也就廢了。