您好,登錄后才能下訂單哦!
Java 中 BIO、NIO和 AIO的深入淺析?很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
Java 中的 BIO、NIO和 AIO 理解為是 Java 語言對操作系統的各種 IO 模型的封裝。程序員在使用這些 API 的時候,不需要關心操作系統層面的知識,也不需要根據不同操作系統編寫不同的代碼。只需要使用Java的API就可以了。
在講 BIO,NIO,AIO 之前先來回顧一下這樣幾個概念:同步與異步,阻塞與非阻塞。 同步與異步
阻塞和非阻塞
BIO (Blocking I/O)
同步阻塞I/O模式,數據的讀取寫入必須阻塞在一個線程內等待其完成。
傳統 BIO
BIO通信(一請求一應答)模型圖如下(圖源網絡,原出處不明):
采用 BIO 通信模型 的服務端,通常由一個獨立的 Acceptor 線程負責監聽客戶端的連接。我們一般通過在while(true) 循環中服務端會調用 accept() 方法等待接收客戶端的連接的方式監聽請求,請求一旦接收到一個連接請求,就可以建立通信套接字在這個通信套接字上進行讀寫操作,此時不能再接收其他客戶端連接請求,只能等待同當前連接的客戶端的操作執行完成, 不過可以通過多線程來支持多個客戶端的連接,如上圖所示。
如果要讓 BIO 通信模型 能夠同時處理多個客戶端請求,就必須使用多線程(主要原因是socket.accept()、socket.read()、socket.write() 涉及的三個主要函數都是同步阻塞的),也就是說它在接收到客戶端連接請求之后為每個客戶端創建一個新的線程進行鏈路處理,處理完成之后,通過輸出流返回應答給客戶端,線程銷毀。這就是典型的 一請求一應答通信模型 。我們可以設想一下如果這個連接不做任何事情的話就會造成不必要的線程開銷,不過可以通過 線程池機制 改善,線程池還可以讓線程的創建和回收成本相對較低。使用FixedThreadPool 可以有效的控制了線程的最大數量,保證了系統有限的資源的控制,實現了N(客戶端請求數量):M(處理客戶端請求的線程數量)的偽異步I/O模型(N 可以遠遠大于 M),下面一節"偽異步 BIO"中會詳細介紹到。
我們再設想一下當客戶端并發訪問量增加后這種模型會出現什么問題?
在 Java 虛擬機中,線程是寶貴的資源,線程的創建和銷毀成本很高,除此之外,線程的切換成本也是很高的。尤其在 Linux 這樣的操作系統中,線程本質上就是一個進程,創建和銷毀線程都是重量級的系統函數。如果并發訪問量增加會導致線程數急劇膨脹可能會導致線程堆棧溢出、創建新線程失敗等問題,最終導致進程宕機或者僵死,不能對外提供服務。
偽異步 IO
為了解決同步阻塞I/O面臨的一個鏈路需要一個線程處理的問題,后來有人對它的線程模型進行了優化一一一后端通過一個線程池來處理多個客戶端的請求接入,形成客戶端個數M:線程池最大線程數N的比例關系,其中M可以遠遠大于N.通過線程池可以靈活地調配線程資源,設置線程的最大值,防止由于海量并發接入導致線程耗盡。
偽異步IO模型圖(圖源網絡,原出處不明):
采用線程池和任務隊列可以實現一種叫做偽異步的 I/O 通信框架,它的模型圖如上圖所示。當有新的客戶端接入時,將客戶端的 Socket 封裝成一個Task(該任務實現java.lang.Runnable接口)投遞到后端的線程池中進行處理,JDK 的線程池維護一個消息隊列和 N 個活躍線程,對消息隊列中的任務進行處理。由于線程池可以設置消息隊列的大小和最大線程數,因此,它的資源占用是可控的,無論多少個客戶端并發訪問,都不會導致資源的耗盡和宕機。
偽異步I/O通信框架采用了線程池實現,因此避免了為每個請求都創建一個獨立線程造成的線程資源耗盡問題。不過因為它的底層仍然是同步阻塞的BIO模型,因此無法從根本上解決問題。
代碼示例
下面代碼中演示了BIO通信(一請求一應答)模型。我們會在客戶端創建多個線程依次連接服務端并向其發送"當前時間+:hello world",服務端會為每個客戶端線程創建一個線程來處理。代碼示例出自閃電俠的博客,原地址如下:
客戶端
/** * * @author 閃電俠 * @date 2018年10月14日 * @Description:客戶端 */ public class IOClient { public static void main(String[] args) { // TODO 創建多個線程,模擬多個客戶端連接服務端 new Thread(() -> { try { Socket socket = new Socket("127.0.0.1", 3333); while (true) { try { socket.getOutputStream().write((new Date() + ": hello world").getBytes()); Thread.sleep(2000); } catch (Exception e) { } } } catch (IOException e) { } }).start(); } }
服務端
/** * @author 閃電俠 * @date 2018年10月14日 * @Description: 服務端 */ public class IOServer { public static void main(String[] args) throws IOException { // TODO 服務端處理客戶端連接請求 ServerSocket serverSocket = new ServerSocket(3333); // 接收到客戶端連接請求之后為每個客戶端創建一個新的線程進行鏈路處理 new Thread(() -> { while (true) { try { // 阻塞方法獲取新的連接 Socket socket = serverSocket.accept(); // 每一個新的連接都創建一個線程,負責讀取數據 new Thread(() -> { try { int len; byte[] data = new byte[1024]; InputStream inputStream = socket.getInputStream(); // 按字節流方式讀取數據 while ((len = inputStream.read(data)) != -1) { System.out.println(new String(data, 0, len)); } } catch (IOException e) { } }).start(); } catch (IOException e) { } } }).start(); } }
總結
在活動連接數不是特別高(小于單機1000)的情況下,這種模型是比較不錯的,可以讓每一個連接專注于自己的 I/O 并且編程模型簡單,也不用過多考慮系統的過載、限流等問題。線程池本身就是一個天然的漏斗,可以緩沖一些系統處理不了的連接或請求。但是,當面對十萬甚至百萬級連接的時候,傳統的 BIO 模型是無能為力的。因此,我們需要一種更高效的 I/O 處理模型來應對更高的并發量。
NIO (no blocking io 也叫 new io)
NIO 即非阻塞IO,是JDK 1.4 更新的api, 核心內容是 將建立連接、數據可讀、可寫等事件交給了操作系統來維護, 通過調用操作系統的 api (如:select、epoll等),來判斷當前是否支持:可讀、可寫,如果當前不可操作,那么直接返回,從而實現了非阻塞。 而不需要像 BIO 那樣每次去輪詢等待連接的建立以及數據的準備是否完成。主要核心的模塊分以下幾類:
1. 緩沖區Buffer
一個特定基類(byte、short、int、long 等)的數據容器,用作在建立socket 連接之后的數據傳輸。
通過 capacity, limit, position,mark 指針來實現數據的讀寫
get()、put() 方法為每個子類都具有的讀、寫數據的api方法,當從當前的 position 讀或寫的同時,position會增加 相應讀寫的數據的長度。當position 達到limit 之后,再次 get、put則會拋出異常
2. Channel 連接通道
一個 channel 代表一個與“實體”的連接通道,如:硬件設備、文件、網絡 socket 。通過連接通道可以使得客戶端-服務器互相傳輸數據,因此通道也是全雙工的(因為是建立在TCP 傳輸層的協議上,因此具備全雙工的能力)。
JDK 中 channel 可以分為以下幾類:
SelectableChannel 用于 阻塞和非阻塞 socket 連接的通道
FileChannel 用于文件操作,包括:reading, writing, mapping, and manipulating a file
3.Selector 多路復用選擇器
用于 SelectableChannel 的多路復用器,當使用非阻塞的 socket 時,需要將監聽的通道 SelectableChannel 感興趣的事件注冊到 selector 多路復用器上(selector 實際上是通過調用操作系統層面的 select、epoll 方法來獲取當前可用的時間)
與之對應的感興趣的事件用 SelectionKey 來表示
處理流程圖:
代碼示例:
// 1.根據操作系統選擇適當的底層 io復用方法 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(8082)); //2.設置為非阻塞 serverSocketChannel.configureBlocking(false); //3.選擇與操作系統適配的選擇器 Selector selector = Selector.open(); //將 serverSocket 的OP_ACCEPT 事件注冊到 selector 選擇器上 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { // 4.監聽當前連接建立情況 int select = selector.select(); if (select > 0) { //判斷連接業務類型 Set<SelectionKey> set = selector.selectedKeys(); Iterator<SelectionKey> iterator = set.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); //建立連接 if (key.isAcceptable()) { ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); //通過 accept 方法獲取與 server端 已經創建好的 socket連接 SocketChannel sc = ssc.accept(); //設置為非阻塞 sc.configureBlocking(false); //注冊感興趣的事件為 READ sc.register(selector, SelectionKey.OP_READ); } //可讀 else if (key.isReadable()) { SocketChannel socket = (SocketChannel) key.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); socket.read(byteBuffer); System.out.println(new String(byteBuffer.array(), StandardCharsets.UTF_8)); key.interestOps(SelectionKey.OP_WRITE); } //可寫 else if (key.isWritable()) { SocketChannel socket = (SocketChannel) key.channel(); socket.write(ByteBuffer.wrap("I'm receive your message".getBytes(StandardCharsets.UTF_8))); socket.close(); System.out.println("連接關閉成功!"); } } } }
AIO(asynchronous io)
NIO 2.0引入了新的異步通道的概念,并提供了異步文件通道和異步套接字通道的實現。
異步的套接字通道時真正的異步非阻塞I/O,對應于UNIX網絡編程中的事件驅動I/O(AIO)。他不需要過多的Selector對注冊的通道進行輪詢即可實現異步讀寫,從而簡化了NIO的編程模型。
代碼示例
private static void server() throws IOException { //根據操作系統建立對應的底層操作類 AsynchronousServerSocketChannel channel = AsynchronousServerSocketChannel.open(); channel.bind(new InetSocketAddress(8082)); while (true) { Future<AsynchronousSocketChannel> future = channel.accept(); try { AsynchronousSocketChannel asc = future.get(); System.out.println("建立連接成功"); Future<Integer> write = asc.write(ByteBuffer.wrap("Now let's exchange datas".getBytes(StandardCharsets.UTF_8))); while (!write.isDone()) { TimeUnit.SECONDS.sleep(2); } System.out.println("發送數據完成"); asc.close(); } catch (Exception e) { e.printStackTrace(); } } } private static void client() throws Exception { AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open(); Future<Void> future = socketChannel.connect(new InetSocketAddress(8082)); while (!future.isDone()) { TimeUnit.SECONDS.sleep(2); } ByteBuffer buffer = ByteBuffer.allocate(1024); Future<Integer> read = socketChannel.read(buffer); while (!read.isDone()) { TimeUnit.SECONDS.sleep(2); } System.out.println("接收服務器數據:" + new String(buffer.array(), 0, read.get())); }
看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。