1. 程式人生 > >java高併發實戰(八)——BIO、NIO和AIO

java高併發實戰(八)——BIO、NIO和AIO

由於之前看的容易忘記,因此特記錄下來,以便學習總結與更好理解,該系列博文也是第一次記錄,所有有好多不完善之處請見諒與留言指出,如果有幸大家看到該博文,希望報以參考目的看瀏覽,如有錯誤之處,謝謝大家指出與留言。

一、什麼是NIO?

NIO是New I/O的簡稱,與舊式的基於流的I/O方法相對,從名字看,它表示新的一套Java I/O標準。是一種多路複用;它是在Java 1.4中被納入到JDK中的,並具有以下特性:

    – NIO是基於塊(Block)的,它以塊為基本單位處理資料而傳統IO是以位元組流的形式,因此NIO效能好一些。

    – 為所有的原始型別提供(Buffer)快取支援– 增加通道(Channel)物件

作為新的原始 I/O 抽象

    – 支援鎖(檔案鎖:有些程式執行時產生.log檔案,他就是檔案鎖,表示當前執行緒在佔用檔案鎖,其他執行緒要使用該鎖時,需要等待阻塞,當使用完鎖時後會刪除該檔案,也就是拿檔案系統來實現鎖)和記憶體對映檔案的檔案訪問介面(一個檔案在硬碟磁碟中的,把檔案對映到記憶體,把檔案讀到記憶體中,比傳統把檔案中資料一個個讀進去快的多)

    – 提供了基於Selector的非同步網路I/O

二、Buffer&Channel


所有的IO/NIO操作(讀寫)都要經過Buffer,他是NIO核心部分。通道就是一個抽象,他的一端可能是一個檔案或socket等,往裡寫資料。也就是說通過對buffer讀寫來對實際的NIO目標來讀取實現等。

緩衝區(Buffer)就是在記憶體中預留指定大小的儲存空間用來對輸入/輸出(I/O)的資料作臨時儲存,這部分預留的記憶體空間就叫做緩衝區:

使用緩衝區有這麼兩個好處:

1、減少實際的物理讀寫次數

2、緩衝區在建立時就被分配記憶體,這塊記憶體區域一直被重用,可以減少動態分配和回收記憶體的次數

舉個簡單的例子,比如A地有1w塊磚要搬到B地

由於沒有工具(緩衝區),我們一次只能搬一本,那麼就要搬1w次(實際讀寫次數)

如果A,B兩地距離很遠的話(IO效能消耗),那麼效能消耗將會很大

但是要是此時我們有輛大卡車(緩衝區),一次可運5000本,那麼2次就夠了

相比之前,效能肯定是大大提高了。

下面是buffer的實現:

1、buffer


buffer簡單的使用:

FileInputStream fin = new FileInputStream(new File("d:\\temp_buffer.tmp"));
FileChannel fc=fin.getChannel();
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);//ByteBuffer.allocate()方法分配了一段記憶體空間,作為快取
fc.read(byteBuffer);
fc.close();
byteBuffer.flip();//讀寫轉換

(1)通過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();
}

(2)Buffer中有3個重要的引數:位置(position)、容量(capactiy)和上限(limit)


通過案例瞭解三個引數的意義:

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();
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());
b.flip();
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());


flip轉換主要兩步操作:把postition置零;把limit前移到10,就是遷移到剛才postition的位置(好處是,下一次操作自然是從postiton的0開始,操作到那一步是有意義的呢,就是limit的位置,換句話說,從postition到limit之間資料是我上一次寫入的的資料而且是有意義的資料,從limit到capacity並不是我上次寫入的資料,可能是沒有意義的資料,因此在此時讀緩衝區,就可以讀postition到limit之間的資料就可以了,因此flip通常用於讀寫轉換,在filp之前用於寫,在filp之後通常用於讀,讀使因為位置已經置零了,而且此時資料到limit是有意義的資料。)。

該操作會重置position,通常,將buffer從寫模式轉換為讀模式時需要執行此方法flip()操作不僅重置了當前的position為0,還將limit設定到當前position的位置。


 public final Buffer rewind()

    – 將position置零,並清除標誌位(mark)就是把之前讀的資料在掃一遍,limit位置是不變的,因此資料是基本一樣的

 public final Buffer clear()

    – 將position置零,同時將limit設定為capacity的大小,並清除了標誌mark

 public final Buffer flip()

    – 先將limit設定到position所在位置,然後將position置零,並清除標誌位mark– 通常在讀寫轉換時使用

(3)檔案對映到記憶體

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();

三、網路程式設計

多執行緒網路伺服器的一般結構


下面實現上面簡單案例:

1.簡單案例 EchoServer

 ServerSocket echoServer = null;
 Socket clientSocket = null;
 try {
 echoServer = new ServerSocket(8000);
 } catch (IOException e) {
 System.out.println(e);
 }
 while (true) {
 try {
 clientSocket = echoServer.accept();
 System.out.println(clientSocket.getRemoteSocketAddress() + " connect!");
 tp.execute(new HandleMsg(clientSocket));
 } catch (IOException e) {
 System.out.println(e);
 }
 }
 }
static class HandleMsg implements Runnable{
省略部分資訊
 public void run(){
 try {
 is = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
 os = new PrintWriter(clientSocket.getOutputStream(), true);
 // 從InputStream當中讀取客戶端所傳送的資料
 String inputLine = null;
 long b=System.currentTimeMillis();//統計時間戳
 while ((inputLine = is.readLine()) != null) {
 os.println(inputLine);
 }
 long e=System.currentTimeMillis();
 System.out.println("spend:"+(e-b)+"ms");
 } catch (IOException e) {
 e.printStackTrace();
 }finally{
關閉資源
}
 }

2.EchoServer的客戶端

public static void main(String[] args) throws IOException {
 Socket client = null;
 PrintWriter writer = null;
 BufferedReader reader = null;
 try {
 client = new Socket();//建立socket
 client.connect(new InetSocketAddress("localhost", 8000));
 writer = new PrintWriter(client.getOutputStream(), true);
 writer.println("Hello!");
 writer.flush();
 reader = new BufferedReader(new InputStreamReader(client.getInputStream()));//讀資料,伺服器返回的資料從socket中
 System.out.println("from server: " + reader.readLine());
 } catch
 } finally {
 //省略資源關閉
 }
}

 問題:

    – 為每一個客戶端使用一個執行緒,如果客戶端出現延時等異常,執行緒可能會被佔用很長時間。因為資料的準備和讀取都在這個執行緒中。

    – 此時,如果客戶端數量眾多,可能會消耗大量的系統資源

 解決

    – 非阻塞的NIO

    – 資料準備好了在工作

3.網路程式設計-模擬低效的客戶端

private static ExecutorService tp=Executors.newCachedThreadPool();
private static final int sleep_time=1000*1000*1000;
public static class EchoClient implements Runnable{
public void run(){
 try {
 client = new Socket();
 client.connect(new InetSocketAddress("localhost", 8000));//開闢若干個執行緒
 writer = new PrintWriter(client.getOutputStream(), true);
 writer.print("H");
 LockSupport.parkNanos(sleep_time);
 writer.print("e");
 LockSupport.parkNanos(sleep_time);
 writer.print("l");
 LockSupport.parkNanos(sleep_time);
 writer.print("l");
 LockSupport.parkNanos(sleep_time);
 writer.print("o");
 LockSupport.parkNanos(sleep_time);
 writer.print("!");
 LockSupport.parkNanos(sleep_time);
 writer.println();
 writer.flush();

伺服器輸出如下:

spend:6000ms
spend:6000ms
spend:6000ms
spend:6001ms
spend:6002ms
spend:6002ms
spend:6002ms
spend:6002ms
spend:6003ms
spend:6003ms

4.網路程式設計-NIO

 把資料準備好了再通知我

    Channel有點類似於流,一個Channel可以和檔案或者網路Socket對應


一個執行緒(也就是selector)可以輪詢多個channel,每一個channl下面對應一個socket。

select()與selectNow()用來看那個channel(客戶頓)準備好資料,selector用select()當資料沒有轉備好他會被阻塞.

selectNow()與select()一樣,但區別是他是無阻賽的,沒有轉備好則返回0;

 總結:

    – NIO會將資料準備好後,再交由應用進行處理(因此資料沒有轉備好他是不會寫入應用中間的,所以節省資源),資料的讀取過程依然在應用執行緒中完成

    – 節省資料準備時間(因為Selector可以複用)

5.網路程式設計 AIO(主要採用非同步回撥方式,他是非同步的IO)

 讀完了再通知我(就會既不需要讀,也不會需要寫,就等待就行了)

 不會加快IO,只是在讀完後進行通知(什麼時候拿到通知什麼時候處理,在沒有拿到通知去做其他事情,節省時間)

 使用回撥函式,進行業務處理

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

使用server上的accept方法


server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
 final ByteBuffer buffer = ByteBuffer.allocate(1024);
 public void completed(AsynchronousSocketChannel result, Object attachment) {
 System.out.println(Thread.currentThread().getName());
 Future<Integer> writeResult=null;
 try {
 buffer.clear();
 result.read(buffer).get(100, TimeUnit.SECONDS);//這裡把非同步有變成了類似同步與等待因為這裡做了個阻塞。在這裡也可以再來個回撥函式去做一些處理
 buffer.flip();
 writeResult=result.write(buffer);
 } catch (InterruptedException | ExecutionException e) {
 e.printStackTrace();
 } catch (TimeoutException e) {
 e.printStackTrace();
 } finally {
 try {
 server.accept(null, this);//用於在做下一個請求操作,不想做完就程式就不在運行了
 writeResult.get();
 result.close();
 } catch (Exception e) {
 System.out.println(e.toString());
 }
 }
 }
 @Override
 public void failed(Throwable exc, Object attachment) {
 System.out.println("failed: " + exc);
 }
});

6、BIO、NIO與AIO對比:

1.同步

例:買飯:自己親自去飯館買飯,這就是同步(自己處理IO讀寫)

2.非同步

例:買飯:叫外賣送到家,這就是非同步(IO讀寫委託給OS處理,需要將資料緩衝區地址和大小傳給OS(飯名和地址),OS需要支援非同步IO操作API)

3.阻塞

例:辦理業務:一直排隊等待(呼叫會一直阻塞到讀寫完成才返回)

4.非阻塞

例:辦理業務:抽號後就可以做其他事,如果你等不急,可以去問工作人員到你了沒,如果沒到你就不能辦理業務。(如果不能讀寫,呼叫會馬上返回,當IO事件分發器會通知可讀寫時再繼續進行讀寫,不斷迴圈直到讀寫完成)

BIO是同步阻塞的

偽非同步IO:通過執行緒池機制優化了BIO模型

NIO是同步非阻塞的

AIO是非同步非阻塞的

由於NIO的讀寫過程依然在應用執行緒裡完成,所以對於那些讀寫過程時間長的,NIO就不太適合。

而AIO的讀寫過程完成後才被通知,所以AIO能夠勝任那些重量級,讀寫過程長的任務

BIO是一個連線一個執行緒,即客戶端有連線請求時伺服器端就需要啟動一個執行緒進行處理,如果這個連線不做任何事情會造成不必要的執行緒開銷,在JDK1.4出來之前使用的BIO

Socket程式設計就是BIO,一個socket連線一個處理執行緒(這個執行緒負責這個Socket連線的一系列資料傳輸操作)。阻塞的原因在於:作業系統允許的執行緒數量是有限的,多個socket申請與服務端建立連線時,服務端不能提供相應數量的處理執行緒,沒有分配到處理執行緒的連線就會阻塞等待或被拒絕。

BIO圖解:


偽非同步IO

通過執行緒池機制優化了BIO模型

偽非同步IO圖解:


NIO模型

同步非阻塞

伺服器實現模式為一個請求一個執行緒,但客戶端傳送的連線請求都會註冊到多路複用器上,多路複用器輪詢到連線有I/O請求時才啟動一個執行緒進行處理。

NIO圖解:

AIO模型

非同步非阻塞

伺服器實現模式為一個有效請求一個執行緒,客戶端的I/O請求都是由OS(作業系統)先完成了再通知伺服器應用去啟動執行緒進行處理,

注:AIO又稱為NIO2.0,在JDK7才開始支援。

AIO流程圖 

BIO、偽非同步、NIO和AIO模型的比較

BIO與NIO一個比較重要的不同,是我們使用BIO的時候往往會引入多執行緒,每個連線一個單獨的執行緒;而NIO則是使用單執行緒或者只使用少量的多執行緒,每個連線共用一個執行緒。

BIO是一個連線一個執行緒。

NIO是一個請求一個執行緒。

AIO是一個有效請求一個執行緒。

Java對BIO、NIO、AIO的支援:

  • Java BIO : 同步並阻塞,伺服器實現模式為一個連線一個執行緒,即客戶端有連線請求時伺服器端就需要啟動一個執行緒進行處理,如果這個連線不做任何事情會造成不必要的執行緒開銷,當然可以通過執行緒池機制改善。

  • Java NIO : 同步非阻塞,伺服器實現模式為一個請求一個執行緒,即客戶端傳送的連線請求都會註冊到多路複用器上,多路複用器輪詢到連線有I/O請求時才啟動一個執行緒進行處理。

  • Java AIO(NIO.2) : 非同步非阻塞,伺服器實現模式為一個有效請求一個執行緒,客戶端的I/O請求都是由OS先完成了再通知伺服器應用去啟動執行緒進行處理,

BIO、NIO、AIO適用場景分析:

  • BIO方式適用於連線數目比較小且固定的架構,這種方式對伺服器資源要求比較高,併發侷限於應用中,JDK1.4以前的唯一選擇,但程式直觀簡單易理解。

  • NIO方式適用於連線數目多且連線比較短(輕操作)的架構,比如聊天伺服器,併發侷限於應用中,程式設計比較複雜,JDK1.4開始支援。

  • AIO方式使用於連線數目多且連線比較長(重操作)的架構,比如相簿伺服器,充分呼叫OS參與併發操作,程式設計比較複雜,JDK7開始支援。

另外,I/O屬於底層操作,需要作業系統支援,併發也需要作業系統的支援,所以效能方面不同作業系統差異會比較明顯。

在高效能的I/O設計中,有兩個比較著名的模式其中Reactor模式用於同步I/O,Proactor運用於非同步I/O操作。

在比較這兩個模式之前,我們首先的搞明白幾個概念,什麼是阻塞和非阻塞,什麼是同步和非同步,同步和非同步是針對應用程式和核心的互動而言的,同步指的是使用者程序觸發IO操作並等待或者輪詢的去檢視IO操作是否就緒,而非同步是指使用者程序觸發IO操作以後便開始做自己的事情,而當IO操作已經完成的時候會得到IO完成的通知。而阻塞和非阻塞是針對於程序在訪問資料的時候,根據IO操作的就緒狀態來採取的不同方式,說白了是一種讀取或者寫入操作函式的實現方式,阻塞方式下讀取或者寫入函式將一直等待,而非阻塞方式下,讀取或者寫入函式會立即返回一個狀態值。

 一般來說I/O模型可以分為:同步阻塞,同步非阻塞,非同步阻塞,非同步非阻塞IO

同步阻塞IO在此種方式下,使用者程序在發起一個IO操作以後,必須等待IO操作的完成,只有當真正完成了IO操作以後,使用者程序才能執行。JAVA傳統的IO模型屬於此種方式!

同步非阻塞IO:在此種方式下,使用者程序發起一個IO操作以後邊可返回做其它事情,但是使用者程序需要時不時的詢問IO操作是否就緒,這就要求使用者程序不停的去詢問,從而引入不必要的CPU資源浪費。其中目前JAVA的NIO就屬於同步非阻塞IO。

非同步阻塞IO此種方式下是指應用發起一個IO操作以後,不等待核心IO操作的完成,等核心完成IO操作以後會通知應用程式,這其實就是同步和非同步最關鍵的區別,同步必須等待或者主動的去詢問IO是否完成,那麼為什麼說是阻塞的呢?因為此時是通過select系統呼叫來完成的,而select函式本身的實現方式是阻塞的,而採用select函式有個好處就是它可以同時監聽多個檔案控制代碼,從而提高系統的併發性!

 非同步非阻塞IO:在此種模式下,使用者程序只需要發起一個IO操作然後立即返回,等IO操作真正的完成以後,應用程式會得到IO操作完成的通知,此時使用者程序只需要對資料進行處理就好了,不需要進行實際的IO讀寫操作,因為真正的IO讀取或者寫入操作已經由核心完成了。目前Java中還沒有支援此種IO模型。   


參考:https://blog.csdn.net/baiye_xing/article/details/73123753

https://blog.csdn.net/skiof007/article/details/52873421

https://www.cnblogs.com/ygj0930/p/6543960.html