IO,NIO,AIO和高性能模型

簡介

首先,傳統的 java.io包,它基於流模型實現,提供了我們最熟知的一些 IO 功能,比如 File 抽象、輸入輸出流等。交互方式是同步、阻塞的方式,也就是說,在讀取輸入流或者寫入輸出流時,在讀、寫動作完成之前,線程會一直阻塞在那裏,它們之間的調用是可靠的線性順序。

java.io包的好處是代碼比較簡單、直觀,缺點則是 IO 效率和擴展性存在侷限性,容易成爲應用性能的瓶頸。

很多時候,人們也把 java.net下面提供的部分網絡 API,比如 Socket、ServerSocket、HttpURLConnection 也歸類到同步阻塞 IO 類庫,因爲網絡通信同樣是 IO 行爲。

第二,在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,可以構建多路複用的、同步非阻塞 IO 程序,同時提供了更接近操作系統底層的高性能數據操作方式。

第三,在 Java 7 中,NIO 有了進一步的改進,也就是 NIO 2,引入了異步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。異步 IO 操作基於事件和回調機制,可以簡單理解爲,應用操作直接返回,而不會阻塞在那裏,當後臺處理完成,操作系統會通知相應線程進行後續工作。

一、IO流(同步、阻塞)
二、NIO(同步、非阻塞)
三、NIO2(異步、非阻塞)

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

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

IO

即原IO,阻塞IO
IO流簡單來說就是input和output流,IO流主要是用來處理設備之間的數據傳輸,Java IO對於數據的操作都是通過流實現的,而java用於操作流的對象都在IO包中。

簡單的描述一下BIO的服務端通信模型:採用BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監聽客戶端的連接,它接收到客戶端連接請求之後爲每個客戶端創建一個新的線程進行鏈路處理沒處理完成後,通過輸出流返回應答給客戶端,線程銷燬。即典型的一請求一應答通宵模型。
在這裏插入圖片描述
該模型最大的問題就是缺乏彈性伸縮能力,當客戶端併發訪問量增加後,服務端的線程個數和客戶端併發訪問數呈1:1的正比關係,Java中的線程也是比較寶貴的系統資源,線程數量快速膨脹後,系統的性能將急劇下降,隨着訪問量的繼續增大,系統最終就死-掉-了

在這裏插入圖片描述

NIO

新IO(reactor模型):線程發起IO請求,立即返回;內核在做好IO操作的準備之後,通過調用註冊的回調函數通知線程做IO操作,線程開始阻塞,直到操作完成

三個主要組成部分:Channel(通道)、Buffer(緩衝區)、Selector(選擇器)

Selector會不斷輪詢註冊在其上的Channel,如果某個Channel上面發生讀或者寫事件,這個Channel就處於就緒狀態,會被Selector輪詢出來,然後通過SelectionKey可以獲取就緒Channel的集合,進行後續的I/O操作。

一個Selector可以同時輪詢多個Channel,因爲JDK使用了epoll()代替傳統的select實現,所以沒有最大連接句柄1024/2048的限制。所以,只需要一個線程負責Selector的輪詢,就可以接入成千上萬的客戶端。
在這裏插入圖片描述
我們知道,如果使用CachedThreadPool線程池(不限制線程數量,如果不清楚請參考文首提供的文章),其實除了能自動幫我們管理線程(複用),看起來也就像是1:1的客戶端:線程數模型,而使用FixedThreadPool我們就有效的控制了線程的最大數量,保證了系統有限的資源的控制,實現了N:M的僞異步I/O模型。

但是,正因爲限制了線程數量,如果發生大量併發請求,超過最大數量的線程就只能等待,直到線程池中的有空閒的線程可以被複用。而對Socket的輸入流就行讀取時,會一直阻塞,直到發生:
在這裏插入圖片描述

有數據可讀 , 可用數據以及讀取完畢 , 發生空指針或I/O異常
所以在讀取數據較慢時(比如數據量大、網絡傳輸慢等),大量併發的情況下,其他接入的消息,只能一直等待,這就是最大的弊端。

而後面即將介紹的NIO,就能解決這個難題。

package com.anxpp.io.calculator.bio;  
import java.io.IOException;  
import java.net.ServerSocket;  
import java.net.Socket;  
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
/** 
 * BIO服務端源碼__僞異步I/O 
 * @author yangtao__anxpp.com 
 * @version 1.0 
 */ 
public final class ServerBetter {  
 //默認的端口號  
 private static int DEFAULT_PORT = 12345;  
 //單例的ServerSocket  
 private static ServerSocket server;  
 //線程池 懶漢式的單例  
 private static ExecutorService executorService = Executors.newFixedThreadPool(60);  
 //根據傳入參數設置監聽端口,如果沒有參數調用以下方法並使用默認值  
 public static void start() throws IOException{  
  //使用默認值  
  start(DEFAULT_PORT);  
 }  
 //這個方法不會被大量併發訪問,不太需要考慮效率,直接進行方法同步就行了  
 public synchronized static void start(int port) throws IOException{  
  if(server != null) return;  
  try{  
//通過構造函數創建ServerSocket  
//如果端口合法且空閒,服務端就監聽成功  
server = new ServerSocket(port);  
System.out.println("服務器已啓動,端口號:" + port);  
//通過無線循環監聽客戶端連接  
//如果沒有客戶端接入,將阻塞在accept操作上。  
while(true){  
 Socket socket = server.accept();  
 //當有新的客戶端接入時,會執行下面的代碼  
 //然後創建一個新的線程處理這條Socket鏈路  
 executorService.execute(new ServerHandler(socket));  
}  
  }finally{  
//一些必要的清理工作  
if(server != null){  
 System.out.println("服務器已關閉。");  
 server.close();  
 server = null;  
}  
  }  
 }  
} 

AIO

我們可以使用線程池來管理這些線程(需要了解更多請參考前面提供的文章),實現1個或多個線程處理N個客戶端的模型(但是底層還是使用的同步阻塞I/O),通常被稱爲“僞異步I/O模型“

AIO是異步IO的縮寫,雖然NIO在網絡操作中,提供了非阻塞的方法,但是NIO的IO行爲還是同步的。對於NIO來說,我們的業務線程是在IO操作準備好時,得到通知,接着就由這個線程自行進行IO操作,IO操作本身是同步的。

但是對AIO來說,則更加進了一步,它不是在IO準備好時再通知線程,而是在IO操作已經完成後,再給線程發出通知。因此AIO是不會阻塞的,此時我們的業務邏輯將變成一個回調函數,等待IO操作完成後,由系統自動觸發。
與NIO不同,當進行讀寫操作時,只須直接調用API的read或write方法即可。這兩種方法均爲異步的,對於讀操作而言,當有流可讀取時,操作系統會將可讀的流傳入read方法的緩衝區,並通知應用程序;對於寫操作而言,當操作系統將write方法傳遞的流寫入完畢時,操作系統主動通知應用程序。 即可以理解爲,read/write方法都是異步的,完成後會主動調用回調函數。 在JDK1.7中,這部分內容被稱作NIO.2,主要在Java.nio.channels包下增加了下面四個異步通道:
AsynchronousSocketChannel
AsynchronousServerSocketChannel
AsynchronousFileChannel
AsynchronousDatagramChannel

在這裏插入圖片描述

高性能模型Reactor 和 Proactor

單服務器高性能的 PPC 和 TPC 模式,它們的優點是實現簡單,缺點是都無法支撐高併發的場景,尤其是互聯網發展到現在,各種海量用戶業務的出現,PPC 和 TPC 完全無能爲力。今天我將介紹可以應對高併發場景的單服務器高性能架構模式:Reactor 和 Proactor。

PPC/TCP 模式最主要的問題就是每個連接都要創建進程(爲了描述簡潔,這裏只以 PPC 和進程爲例,實際上換成 TPC 和線程,原理是一樣的),連接結束後進程就銷燬了,這樣做其實是很大的浪費。爲了解決這個問題,一個自然而然的想法就是資源複用,即不再單獨爲每個連接創建進程,而是創建一個進程池,將連接分配給進程,一個進程可以處理多個連接的業務。

爲了能夠更好地解決上述問題,很容易可以想到,只有當連接上有數據的時候進程纔去處理,這就是 I/O 多路複用技術的來源。

I/O 多路複用技術歸納起來有兩個關鍵實現點:

  • 當多條連接共用一個阻塞對象後,進程只需要在一個阻塞對象上等待,而無須再輪詢所有連接,常見的實現方式有 select、epoll、kqueue 等。
  • 當某條連接有新的數據可以處理時,操作系統會通知進程,進程從阻塞狀態返回,開始進行業務處理。

Reactor 模式有這三種典型的實現方案:

  • 單 Reactor 單進程 / 線程
  • 單 Reactor 多線程
  • 多 Reactor 多進程 / 線程

以上方案具體選擇進程還是線程,更多地是和編程語言及平臺相關。例如,Java 語言一般使用線程(例如,Netty),C 語言使用進程和線程都可以。例如,Nginx 使用進程,Memcache 使用線程。

單 Reactor 單進程 / 線程

在這裏插入圖片描述
單 Reactor 單進程的方案在實踐中應用場景不多,只適用於業務處理非常快速的場景,目前比較著名的開源軟件中使用單 Reactor 單進程的是 Redis。

需要注意的是,C 語言編寫系統的一般使用單 Reactor 單進程,因爲沒有必要在進程中再創建線程;而 Java 語言編寫的一般使用單 Reactor 單線程,因爲 Java 虛擬機是一個進程,虛擬機中有很多線程,業務線程只是其中的一個線程而已。

單 Reactor 單進程的模式優點就是很簡單,沒有進程間通信,沒有進程競爭,全部都在同一個進程內完成。但其缺點也是非常明顯,具體表現有:

  • 只有一個進程,無法發揮多核 CPU 的性能;只能採取部署多個系統來利用多核 CPU,但這樣會帶來運維複雜度,本來只要維護一個系統,用這種方式需要在一臺機器上維護多套系統。
  • Handler 在處理某個連接上的業務時,整個進程無法處理其他連接的事件,很容易導致性能瓶頸。

單 Reactor 多線程

在這裏插入圖片描述
單 Reator 多線程方案能夠充分利用多核多 CPU 的處理能力,但同時也存在下面的問題:

  • 多線程數據共享和訪問比較複雜。例如,子線程完成業務處理後,要把結果傳遞給主線程的 Reactor 進行發送,這裏涉及共享數據的互斥和保護機制。以 Java 的 NIO 爲例,Selector 是線程安全的,但是通過 Selector.selectKeys() 返回的鍵的集合是非線程安全的,對 selected keys 的處理必須單線程處理或者採取同步措施進行保護。
  • Reactor 承擔所有事件的監聽和響應,只在主線程中運行,瞬間高併發時會成爲性能瓶頸。

你可能會發現,我只列出了“單 Reactor 多線程”方案,沒有列出“單 Reactor 多進程”方案,這是什麼原因呢?主要原因在於如果採用多進程,子進程完成業務處理後,將結果返回給父進程,並通知父進程發送給哪個 client,這是很麻煩的事情。因爲父進程只是通過 Reactor 監聽各個連接上的事件然後進行分配,子進程與父進程通信時並不是一個連接。如果要將父進程和子進程之間的通信模擬爲一個連接,並加入 Reactor 進行監聽,則是比較複雜的。而採用多線程時,因爲多線程是共享數據的,因此線程間通信是非常方便的。雖然要額外考慮線程間共享數據時的同步問題,但這個複雜度比進程間通信的複雜度要低很多。

多 Reactor 多進程 / 線程

在這裏插入圖片描述
多 Reactor 多進程 / 線程的方案看起來比單 Reactor 多線程要複雜,但實際實現時反而更加簡單,主要原因是:

  • 父進程和子進程的職責非常明確,父進程只負責接收新連接,子進程負責完成後續的業務處理。
  • 父進程和子進程的交互很簡單,父進程只需要把新連接傳給子進程,子進程無須返回數據。
  • 子進程之間是互相獨立的,無須同步共享之類的處理(這裏僅限於網絡模型相關的 select、read、send 等無須同步共享,“業務處理”還是有可能需要同步共享的)。

目前著名的開源系統 Nginx 採用的是多 Reactor 多進程,採用多 Reactor 多線程的實現有 Memcache 和 Netty。

我多說一句,Nginx 採用的是多 Reactor 多進程的模式,但方案與標準的多 Reactor 多進程有差異。具體差異表現爲主進程中僅僅創建了監聽端口,並沒有創建 mainReactor 來“accept”連接,而是由子進程的 Reactor 來“accept”連接,通過鎖來控制一次只有一個子進程進行“accept”,子進程“accept”新連接後就放到自己的 Reactor 進行處理,不會再分配給其他子進程,更多細節請查閱相關資料或閱讀 Nginx 源碼。

Proactor

在這裏插入圖片描述
Reactor 是非阻塞同步網絡模型,因爲真正的 read 和 send 操作都需要用戶進程同步操作。這裏的“同步”指用戶進程在執行 read 和 send 這類 I/O 操作的時候是同步的,如果把 I/O 操作改爲異步就能夠進一步提升性能,這就是異步網絡模型 Proactor。

Proactor 中文翻譯爲“前攝器”比較難理解,與其類似的單詞是 proactive,含義爲“主動的”,因此我們照貓畫虎翻譯爲“主動器”反而更好理解。Reactor 可以理解爲“來了事件我通知你,你來處理”,而 Proactor 可以理解爲“來了事件我來處理,處理完了我通知你”。這裏的“我”就是操作系統內核,“事件”就是有新連接、有數據可讀、有數據可寫的這些 I/O 事件,“你”就是我們的程序代碼。

大白話:來了事件我來處理,處理完了我通知你

原文資料

https://blog.csdn.net/zhangbijun1230/article/details/90739882
https://www.2cto.com/kf/201808/766978.html

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章