java高併發程序設計學習筆記八BIO、NIO和AIO

BIO:

Blocking I/O,傳統的同步阻塞式網絡編程;

網絡編程的基本模型是C/S模型,即兩個進程間的通信;服務端提供IP和監聽端口,客戶端通過連接向服務端發起連接請求,通過三次握手連接,若成功則雙方通過套接字進行通信;

採用BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監聽客戶端的連接,它接收到客戶端連接請求之後爲每個客戶端創建一個新的線程,

進行鏈路處理,處理完成後,通過輸出流返回應答給客戶端,線程銷燬。即典型的一請求一應答通宵模型。

問題是:

缺乏彈性伸縮能力,當客戶端併發訪問量增加後,服務端的線程個數和客戶端併發訪問數呈1:1的正比關係;

線程數量快速膨脹後,系統的性能將急劇下降,隨着訪問量的繼續增大,系統最終就死-掉-了


 僞異步I/O編程
爲了改進這種一連接一線程的模型,使用線程池來管理這些線程,實現1個或多個線程處理N個客戶端的模型(但是底層還是使用的同步阻塞I/O),

通常被稱爲“僞異步I/O模型“。
不能使用使用CachedThreadPool線程池,建議FixedThreadPool,

如果發生大量併發請求,超過最大數量的線程就只能等待,直到線程池中的有空閒的線程可以被複用。

在讀取數據較慢時(比如數據量大、網絡傳輸慢等),大量併發的情況下,其他接入的消息,只能一直等待,這是問題。


什麼是NIO?

New I/O簡稱,或Non-block I/O,與舊式的基於流的I/O方法相對,表示一套新的I/O標準;jdk1.4引入的;

NIO提供了與傳統BIO模型中的Socket和ServerSocket相對應的SocketChannel和ServerSocketChannel,兩種不同的套接字通道實現。

新增的着兩種通道都支持阻塞和非阻塞兩種模式,阻塞模式使用就像傳統中的支持一樣,比較簡單,但是性能和可靠性都不好;非阻塞模式正好與之相反

  對於低負載、低併發的應用程序,可以使用同步阻塞I/O來提升開發速率和更好的維護性;

對於高負載、高併發的(網絡)應用,應使用NIO的非阻塞模式來開發。

NIO是基於塊(Block)的,以塊爲基本單位處理數據,傳統I/O基於字節(Byte)的;性能比流好些;

爲所有原始類型提供(Buffer)緩存支持;

增加通道(Channel)對象,作爲新的I/O抽象,類似舊式的Stream;

支持文件鎖,和內存映射文件的文件訪問接口;使用文件來實現鎖;

基於網絡操作,提供了Selector的異步網絡I/O;


Buffer(緩存區)&Channel(通道)

Buffer:NIO的核心部分,所有IO操作部分都要Buffer做,與Channel(通道)交互;

緩衝區實際上是一個數組,並提供了對數據結構化訪問以及維護讀寫位置等信息。

Channel,通道是IO的抽象,另一端就是要操作的對象或socket;

通過對buffer的讀取,來實現對IO的操作;

ByteBuffer、CharBuffer,DoubleBuffer,IntBuffer,LongBuffer等,實現了Buffer

Channel我們對數據的讀取和寫入要通過Channel,它就像水管一樣,是一個通道。

通道不同於流的地方就是通道是雙向的,可以用於讀、寫和同時讀寫操作。

底層的操作系統的通道一般都是全雙工的,所以全雙工的Channel比流能更好的映射底層操作系統的API。

Channel主要分兩大類:

SelectableChannel:用戶網絡讀寫(子類有:ServerSocketChannel和SocketChannel);

FileChannel:用於文件操作;


基本使用一:

FileInputStream fin = new FileInputStream(new File("d:\\temp_buffer.tmp"));
FileChannel fc=fin.getChannel();

ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
fc.read(byteBuffer);

fc.close();
byteBuffer.flip();
 


基本使用二:NIO複製文件

public static void nioCopyFile(String resource, String destination)
throws IOException {
FileInputStream fis = new FileInputStream(resource);
FileOutputStream fos = new FileOutputStream(destination);
FileChannel readChannel = fis.getChannel(); //讀文件通道
FileChannel writeChannel = fos.getChannel(); //寫文件通道
ByteBuffer buffer = ByteBuffer.allocate(1024); //讀入數據緩存
while (true) {
buffer.clear();
int len = readChannel.read(buffer); //讀入數據
if (len == -1) {
break;
//讀取完畢
}
buffer.flip();
writeChannel.write(buffer);//寫入文件
}
readChannel.close();
writeChannel.close();
}
 


Buffer中3個重要參數:位置(position)、容量(capacity)、上限(limit);
位置:記錄當前緩衝區的位置,將從此位置開始讀寫數據;

容量:緩存區的總容量上限;

上限:緩存區實際數據的大小,可讀取的容量,總是小於等於容量;通常情況和容量相等;


Buffer的3個函數:

flip():讀寫轉換時使用;先將limit設置爲當前position的位置(實際大小),再將position重置爲0,清除標誌位Mark

rewind():將position置零,並清除標誌位;

clear():將position置零,同時將limit設置爲capacity大小,清除標誌位Mark;


實例展示:

ByteBuffer b=ByteBuffer.allocate(15); //15個字節大小的緩衝區  
System.out.println("limit="+b.limit()+"capacity="+b.capacity()+"position="+b.position();
for(int i=0;i<10;i++){ //存入10個字節數據
b.put((byte)i);
}
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());
b.flip(); //重置position  
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());
for(int i=0;i<5;i++){
System.out.print(b.get());
}

System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());


文件映射到內存實例:

RandomAccessFile raf = new RandomAccessFile("C:\\mapfile.txt", "rw");
FileChannel fc = raf.getChannel(); //將文件映射到內存中

MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, raf.length());

while(mbb.hasRemaining()){
System.out.print((char)mbb.get());
}
mbb.put(0,(byte)98); //修改文件
raf.close();


多路複用器 Selector

NIO的基礎;

Selector提供選擇已經就緒的任務的能力:Selector會不斷輪詢註冊在其上的Channel;

如果某個Channel上面發生讀或者寫事件,這個Channel就處於就緒狀態,會被Selector輪詢出來,

然後通過SelectionKey可以獲取就緒Channel的集合,進行後續的I/O操作。

一個Selector可以同時輪詢多個Channel,因爲JDK使用了epoll()代替傳統的select實現,所以沒有最大連接句柄1024/2048的限制。

所以,只需要一個線程負責Selector的輪詢,就可以接入成千上萬的客戶端。

網絡編程

多線程網絡服務器的一般結構:

(圖,N<--->N)

問題:

爲每一個客戶端開啓一個線程,如果客戶端延時異常,線程可能被長時間佔用,因爲數據的準備和讀取都在這個線程中,佔用時間;

若客戶端數量衆多,可能消耗大量的系統資源;

解決:

非阻塞的NIO;

數據準備好了再工作;


NIO做法:把數據準備好了再通知我;

Channel類似於流,一個Channel可以和文件或者網絡Socket對應;

一個Selector對應一個線程,可以輪詢多個Channel,一個Channel背後對應一個Socket,即一個客戶端;

所以一個線程可以輪詢了多個channel,選擇準備好數據的Channel;

Selector的兩個方法,

select():若沒有準備好數據的Channel,則阻塞;準備好數據後返回SelectionKey,表示一對關係selector和channel的對應;

selectNow():也是選擇已經準備好的channel,若沒有準備好的,則直接返回,不阻塞;


代碼實例:

一定要寫個例子看看哦;


總結:

NIO會將數據準備好後再交給應用處理,數據讀取過程依然在線程中完成;

節省數據準備時間(因爲selector可以複用);


AIO

NIO是剝離了數據準備時,避免大量線程的等待操作,數據讀寫處理還是在線程應用中完成;

AIO更進一步,在數據準備及讀寫時都不處理,等數據操作完成後,加入回調函數即可;異步操作IO;

IO本身的速度是沒有改變的,這幾種方式只是改變裏處理IO的方式,改善線程調度,看起來系統性能變高;

server = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(PORT));

使用server上的accept()方法:

public abstract <A> void accept(A attachment, CompletionHandler<AsynchronousSocketChannel,?super A> handler);

還有read()、 write() 方法;AsynchronousSocketChannel.read(ByteBuffer b, ...)


爲什麼需要了解NIO和AIO?

AIO是真正的異步非阻塞的,所以,在面對超級大量的客戶端,更能得心應手。

03

具體選擇什麼樣的模型或者NIO框架,完全基於業務的實際應用場景和性能需求,

如果客戶端很少,服務器負荷不重,就沒有必要選擇開發起來相對不那麼簡單的NIO做服務端;

相反,就應考慮使用NIO或者相關的框架了





發佈了64 篇原創文章 · 獲贊 45 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章