java高併發實戰(八)——BIO、NIO和AIO
由於之前看的容易忘記,因此特記錄下來,以便學習總結與更好理解,該系列博文也是第一次記錄,所有有好多不完善之處請見諒與留言指出,如果有幸大家看到該博文,希望報以參考目的看瀏覽,如有錯誤之處,謝謝大家指出與留言。
一、什麼是NIO?
NIO是New I/O的簡稱,與舊式的基於流的I/O方法相對,從名字看,它表示新的一套Java I/O標準。是一種多路複用;它是在Java 1.4中被納入到JDK中的,並具有以下特性:
– NIO是基於塊(Block)的,它以塊為基本單位處理資料而傳統IO是以位元組流的形式,因此NIO效能好一些。
– 為所有的原始型別提供(Buffer)快取支援– 增加通道(Channel)物件
– 支援鎖(檔案鎖:有些程式執行時產生.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