日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > linux >内容正文

linux

bio linux 创建_不断升级,Java之BIO、NIO、AIO的演变

發布時間:2025/4/5 linux 63 豆豆
生活随笔 收集整理的這篇文章主要介紹了 bio linux 创建_不断升级,Java之BIO、NIO、AIO的演变 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一、前言

一句話概括BIO NIO AIO:
第一階段,服務端采用同步阻塞的BIO;
第二階段,服務端采用同步阻塞的線程池的BIO;
第三階段,JDK4之后服務端采用同步非阻塞的NIO;
第四階段,JDK7之后服務端采用異步非阻塞的AIO。

Java BIO 對應 Linux 同步非阻塞IO,
Java NIO 對應 Linux 信號驅動IO,
Java AIO 對應 Linux 異步IO。

二、手寫BIO(等待數據阻塞、數據從內核復制到用戶空間阻塞)

2.1 曙光:同步與異步,阻塞與非阻塞

同步與異步

同步: 同步就是發起一個調用后,被調用者未處理完請求之前,調用不返回。
異步: 異步就是發起一個調用后,立刻得到被調用者的回應表示已接收到請求,但是被調用者并沒有返回結果,此時我們可以處理其他的請求,被調用者通常依靠事件,回調等機制來通知調用者其返回結果。
小結:同步和異步的區別最大在于異步的話調用者不需要等待處理結果,被調用者會通過回調等機制來通知調用者其返回結果。

阻塞和非阻塞

阻塞: 阻塞就是發起一個請求,調用者一直等待請求結果返回,也就是當前線程會被掛起,無法從事其他任務,只有當條件就緒才能繼續。
非阻塞: 非阻塞就是發起一個請求,調用者不用一直等著結果返回,可以先去干其他事情。
小結:同步和阻塞對應,異步和非阻塞對應,但是也存在同步非阻塞,間隔時間就調用。

tip1:同步與異步,對于響應方來說的,同步響應還是異步響應;阻塞和非阻塞,對于請求方來說的,阻塞請求方還是不阻塞請求方。

tip2:對于等待數據階段,Linux五種IO中,第一種同步阻塞,第二種同步非阻塞,第三種基于消息通信的異步非阻塞;對于等待數據階段+復制數據階段,Linux五種IO中,第一種到第四種都是同步阻塞,因為復制數據的時間都是同步阻塞,只有第五種才是真正意義上的異步非阻塞。

2.2 概要:BIO

同步阻塞I/O模式,一個請求一個應答,數據的讀取寫入必須阻塞在一個線程內等待其完成。

采用 BIO 通信模型 的服務端,通常由一個獨立的 Acceptor 線程負責監聽客戶端的連接。我們一般通過在 while(true) 循環中服務端會調用 accept() 方法等待接收客戶端的連接的方式監聽請求,請求一旦接收到一個連接請求,就可以建立通信套接字在這個通信套接字上進行讀寫操作,此時不能再接收其他客戶端連接請求,只能等待同當前連接的客戶端的操作執行完成, 不過可以通過多線程來支持多個客戶端的連接,如上圖所示。

如果要讓 BIO 通信模型 能夠同時處理多個客戶端請求,就必須使用多線程(主要原因是 socket.accept()、 socket.read()、 socket.write() 涉及的三個主要函數都是同步阻塞的),也就是說它在接收到客戶端連接請求之后為每個客戶端創建一個新的線程進行鏈路處理,處理完成之后,通過輸出流返回應答給客戶端,線程銷毀。這就是典型的 一請求一應答通信模型 。我們可以設想一下如果這個連接不做任何事情的話就會造成不必要的線程開銷,不過可以通過 線程池機制 改善,線程池還可以讓線程的創建和回收成本相對較低。使用FixedThreadPool 可以有效的控制了線程的最大數量,保證了系統有限的資源的控制,實現了N(客戶端請求數量):M(處理客戶端請求的線程數量)的偽異步I/O模型(N 可以遠遠大于 M),下面一節"偽異步 BIO"中會詳細介紹到。

我們再設想一下當客戶端并發訪問量增加后這種模型會出現什么問題?

在 Java 虛擬機中,線程是寶貴的資源,線程的創建和銷毀成本很高,除此之外,線程的切換成本也是很高的。尤其在 Linux 這樣的操作系統中,線程本質上就是一個進程,創建和銷毀線程都是重量級的系統函數。如果并發訪問量增加會導致線程數急劇膨脹可能會導致線程堆棧溢出、創建新線程失敗等問題,最終導致進程宕機或者僵死,不能對外提供服務。

2.3 手寫BIO:服務端使用同步阻塞單線程的BIO,客戶端使用命令行連接

public class BIOServer { public static void main(String[] args) { try (ServerSocket serverSocket = new ServerSocket(8081)) { System.out.println("服務端端口號:" + serverSocket.getLocalSocketAddress()); while (true) { Socket clientSocket = serverSocket.accept(); System.out.println("連接來自:" + clientSocket.getRemoteSocketAddress()); try (Scanner input = new Scanner(clientSocket.getInputStream())) { while (true) { String request = input.nextLine(); if ("quit".equals(request)) break; System.out.println("客戶端端口:" + clientSocket.getLocalSocketAddress()); System.out.println("客戶端輸入:" + request); String response = "服務端響應" + request; clientSocket.getOutputStream().write(response.getBytes()); } } } } catch (Exception e) { e.printStackTrace(); } }}


運行成功

2.4 概要:偽異步IO

為了解決同步阻塞I/O面臨的一個鏈路需要一個線程處理的問題,后來有人對它的線程模型進行了優化一一一后端通過一個線程池來處理多個客戶端的請求接入,形成客戶端個數M:線程池最大線程數N的比例關系,其中M可以遠遠大于N.通過線程池可以靈活地調配線程資源,設置線程的最大值,防止由于海量并發接入導致線程耗盡。

采用線程池和任務隊列可以實現一種叫做偽異步的 I/O 通信框架,它的模型圖如上圖所示。當有新的客戶端接入時,將客戶端的 Socket 封裝成一個Task(該任務實現java.lang.Runnable接口,變成一個可以多線程的類)投遞到后端的線程池中進行處理,JDK 的線程池維護一個消息隊列和 N 個活躍線程,對消息隊列中的任務進行處理。由于線程池可以設置消息隊列的大小和最大線程數,因此,它的資源占用是可控的,無論多少個客戶端并發訪問,都不會導致資源的耗盡和宕機。

偽異步I/O通信框架采用了線程池實現,因此避免了為每個請求都創建一個獨立線程造成的線程資源耗盡問題。不過因為它的底層任然是同步阻塞的BIO模型,因此無法從根本上解決問題。

2.5 手寫偽異步IO:服務端使用同步阻塞多線程的BIO,客戶端使用命令行連接

public class ClientHandler implements Runnable { //這就是Task private final Socket clientSocket; // Task類中注入一個Socket 是客戶端socket public ClientHandler(Socket clientSocket) { this.clientSocket = clientSocket; } @Override public void run() { //Task類中國的run()方法 try (Scanner input = new Scanner(clientSocket.getInputStream())) { //讀取輸入流 while (true) { String request = input.nextLine(); // 讀取輸入的數據 if ("quit".equals(request)) break; //如果輸入的是quit,表示客戶端程序結束了,這邊結束服務端程序 System.out.println("客戶端端口:" + clientSocket.getLocalSocketAddress()+ " 客戶端輸入:" + request); // 打印收到的請求報文 String response = "server response: " + request; //組裝響應體 clientSocket.getOutputStream().write(response.getBytes()); // 發送響應體 } } catch (Exception e) { System.out.println(e); } }}public class ServerThreadPool { public static void main(String[] args){ ExecutorService executor= Executors.newFixedThreadPool(3); // 擁有三個線程的executor,在哪里使用 try(ServerSocket serverSocket=new ServerSocket(8082)){ // 新建服務端socket System.out.println("服務端端口號:"+serverSocket.getLocalSocketAddress()); while(true){ Socket clientSocket=serverSocket.accept(); // 服務端socket接收請求 System.out.println("連接來自:"+clientSocket.getRemoteSocketAddress()); // 通過accept阻塞后,打印收到的請求 executor.execute(new ClientHandler(clientSocket)); // 這里使用executor,當還有空閑線程的時候,使用空閑線程執行run方法來執行read write,沒有空閑線程,這里阻塞 } }catch (Exception e){ e.printStackTrace(); } }}

其實,Client不是一定要用命令行,Client代碼也可以自己寫出來。在客戶端創建多個線程依次連接服務端并向其發送"當前時間+:hello world",服務端會為每個客戶端線程創建一個線程來處理,如下:

客戶端代碼代替命令行

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 { // 沒休眠兩秒鐘,不斷write到服務端 socket.getOutputStream().write((new Date() + ": hello world").getBytes()); Thread.sleep(2000); } catch (Exception e) { } } } catch (IOException e) { } }).start(); }}

服務端

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(); // 子線程接收請求,不會阻塞main線程 // 每一個新的連接都創建一個線程,負責讀取數據,接受請求后,新建一個子線程來處理read write 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(等待數據不阻塞、數據從內核復制到用戶空間阻塞)

3.1 NIO:JDK4之后服務端采用同步非阻塞NIO(客戶端不變)

NIO是一種同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,對應 java.nio 包,提供了 Channel , Selector,Buffer等抽象。

NIO中的N可以理解為Non-blocking,不單純是New。它支持面向緩沖的,基于通道的I/O操作方法。 NIO提供了與傳統BIO模型中的 Socket 和 ServerSocket 相對應的 SocketChannel 和 ServerSocketChannel 兩種不同的套接字通道實現,兩種通道都支持阻塞和非阻塞兩種模式。阻塞模式使用就像傳統中的支持一樣,比較簡單,但是性能和可靠性都不好;非阻塞模式正好與之相反。對于低負載、低并發的應用程序,可以使用同步阻塞I/O來提升開發速率和更好的維護性;對于高負載、高并發的(網絡)應用,應使用 NIO 的非阻塞模式來開發。

tip1:服務端channel和客戶端channel:服務端只有啟動的時候有一個channel,但是,服務端每收到一個客戶端連接,就有一個客戶端channel,n個客戶端連接就有n個客戶端channel,一個客戶端斷開連接會銷毀這個客戶端channel,同時取消在select中的selectionKey。
tip2:服務端channel和客戶端channel設置為非阻塞的時機:服務端channel在啟動五步中就設置為非阻塞,accept連接之后得到的客戶端channel設置為非阻塞;

3.2 服務端使用同步非阻塞單線程的NIO(Reactor單線程模式,一個線程接收請求,一個線程執行操作),客戶端使用命令行連接

public class NioServer { //三要素 Channel Selector Buffer public static void main(String[] args) throws Exception { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // channel.open() serverSocketChannel.configureBlocking(false); // 設置為阻塞為false serverSocketChannel.bind(new InetSocketAddress(8083)); // 設置服務端端口 System.out.println("服務端端口:" + serverSocketChannel.getLocalAddress()); // 打印服務端ip:port Selector selector = Selector.open(); // selector.open()得到一個selector對象 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);// channel注冊到selector中 ByteBuffer byteBuffer = ByteBuffer.allocate(1024);//新建1024 Bytebuffer 緩沖區 while (true) { // 服務端永不停止 int select = selector.select(); // selector選擇 這里阻塞,要有客戶端連接accept,或者客戶端輸入才能通過 if (0 == select) continue; // Set selectionKeys = selector.selectedKeys(); // 得到一個SelectKey集合 Iterator iterator = selectionKeys.iterator(); //得到對應的迭代器,用來遍歷 while (iterator.hasNext()) { SelectionKey key = iterator.next(); // 遍歷每一個key if (key.isAcceptable()) { // key是可接受的那就接受 key.isAcceptable() 和 channel.accept() ServerSocketChannel channel = (ServerSocketChannel) key.channel(); // 服務端的channel SocketChannel clientChannel = channel.accept(); // 客戶端的SocketChannel,類似io中的Socket System.out.println("客戶端接口:" + clientChannel.getRemoteAddress()); // 從客戶端的SocketChannel打印客戶端ip:port clientChannel.configureBlocking(false); // 客戶端channel設置阻塞為false clientChannel.register(selector, SelectionKey.OP_READ); // 客戶端Socketchannel注冊到selector } if (key.isReadable()) { // 是可讀的就讀 key.isReadable() 到 channel.read(byteBuffer) SocketChannel channel = (SocketChannel) key.channel(); channel.read(byteBuffer); // 讀取buffer里面的內容,從客戶端讀入,這個key已經建立連接,所以使用channel讀入 String request = new String(byteBuffer.array()).trim(); byteBuffer.clear(); // 每次循環之前或之后要清空buffer System.out.println("客戶端端口:" + channel.getRemoteAddress() + " 客戶端輸入:" + request); String response = "server request" + request; channel.write(ByteBuffer.wrap(response.getBytes())); // 寫出到客戶端 } iterator.remove(); //一定要使用迭代器刪除 } } }}

運行結果:

實際上,accept和讀寫操作可以交給子線程來完成,不一定要在main線程上完成,如下:

public class NIOServer { public static void main(String[] args) throws IOException { // 1. serverSelector負責輪詢是否有新的連接,服務端監測到新的連接之后,不再創建一個新的線程, // 而是直接將新連接綁定到clientSelector上,這樣就不用 IO 模型中 1w 個 while 循環在死等 Selector serverSelector = Selector.open(); // 2. clientSelector負責輪詢連接是否有數據可讀 Selector clientSelector = Selector.open(); new Thread(() -> { try { // 對應IO編程中服務端啟動 ServerSocketChannel listenerChannel = ServerSocketChannel.open(); // 打開 listenerChannel.socket().bind(new InetSocketAddress(3333)); listenerChannel.configureBlocking(false); listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT); // 服務端channel注冊到服務端selector上面去 while (true) { // 監測是否有新的連接,這里的1指的是阻塞的時間為 1ms if (serverSelector.select(1) > 0) { // 服務端select Set set = serverSelector.selectedKeys(); Iterator keyIterator = set.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isAcceptable()) { try { // (1) 每來一個新連接,不需要創建一個線程,而是直接注冊到clientSelector SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept(); clientChannel.configureBlocking(false); clientChannel.register(clientSelector, SelectionKey.OP_READ); // 客戶端selector注冊到客戶端selector上面去 } finally { keyIterator.remove(); } } } } } } catch (IOException ignored) { } }).start(); new Thread(() -> { // 這個線程負責客戶端讀寫 try { while (true) { // (2) 批量輪詢是否有哪些連接有數據可讀,這里的1指的是阻塞的時間為 1ms if (clientSelector.select(1) > 0) { Set set = clientSelector.selectedKeys(); Iterator keyIterator = set.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isReadable()) { try { SocketChannel clientChannel = (SocketChannel) key.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); // (3) 面向 Buffer clientChannel.read(byteBuffer); byteBuffer.flip(); System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer).toString()); } finally { keyIterator.remove(); key.interestOps(SelectionKey.OP_READ); } } } } } } catch (IOException ignored) { } }).start(); }}

3.3 合理封裝:Reactor單線程模型的NIO,一個線程接收請求accept,一個線程執行IO操作read-write

import java.io.IOException;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.channels.*;import java.nio.charset.Charset;import java.util.Iterator;import java.util.Set;public class NIOServer { private static final Charset charset = Charset.forName("utf-8"); private static Selector selector ; static { try { selector = Selector.open(); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { try { // channel起手四句:ServerSocketChannel.open(); channel設置阻塞為false // channel.bind 端口號 channel.register(selector) ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.bind(new InetSocketAddress(8080)); serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT); new Reactor().start(); } catch (IOException e) { e.printStackTrace(); } } private static class Reactor extends Thread{ @Override public void run() { try { select(); } catch (IOException e) { e.printStackTrace(); } } private void select() throws IOException{ while (selector.select()>0){ //如果沒有準備好的通道,這里會阻塞住,減少CPU消耗,有準備好的通過,馬上使用 Set selectionKeys = selector.selectedKeys(); Iterator iterator = selectionKeys.iterator(); while (iterator.hasNext()){ SelectionKey key = iterator.next(); if (key.isAcceptable()){ accept(key); }else if (key.isReadable()){ read(key); } iterator.remove(); //必須要從集合中移除!否則下次事件發生時不會被感知到 } } } private void read(SelectionKey key) throws IOException { SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(100); int num = channel.read(byteBuffer); if (num > 0) { byteBuffer.flip(); String data = charset.decode(byteBuffer).toString(); // 解析出來 System.out.println(data); //打印出來 }else if (num == -1){ // num=-1代表連接已關閉 channel.close(); } } private void accept(SelectionKey key) throws IOException { ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel(); SocketChannel channel = serverChannel.accept(); if (channel != null) { InetSocketAddress localAddress = (InetSocketAddress) channel.getLocalAddress(); String hostName = localAddress.getHostName(); System.out.println("接收到來自" + hostName + "的請求"); channel.configureBlocking(false); //監聽這個接收到的socket channel.register(selector, SelectionKey.OP_READ); // 注冊到客戶端channel上面去 } } }}

注意1:這里將所有發生的事件都交給單個線程去處理,如果性能不夠,可以開個線程池去處理事件,
比如,Reactor三種模式:單線程、線程池、主從線程池。

注意2:這里的select模型和redis單線程處理多個連接請求有相似之處;

注意3:解釋一下 while (selector.select()>0)
對于while (selector.select()>0)這句,表示如果沒有準備好的通道,這里會阻塞住,減少CPU消耗,有準備好的通過,馬上使用。

問題:為什么大家都不愿意用 JDK 原生 NIO 進行開發呢?
回答:
1、原生NIO開發代碼比較復雜,需要熟練Channel Buffer Selector的使用,增加程序員的負擔;
2、JDK 的 NIO 底層由 epoll 實現,該實現飽受詬病的空輪詢 bug 會導致 cpu 飆升 100%;
3、已封裝的框架做好了,Netty 的出現很大程度上改善了 JDK 原生 NIO 所存在的一些讓人難以忍受的問題,完成封裝好了NIO的操作,提供了Reactor三種模式的實現。

四、手寫AIO(等待數據不阻塞、數據從內核復制到用戶空間不阻塞)

4.1 概要:AIO

AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改進版 NIO 2,它是異步非阻塞的IO模型。異步 IO 是基于事件和回調機制實現的,也就是應用操作之后會直接返回,不會堵塞在那里,當后臺處理完成,操作系統會通知相應的線程進行后續的操作。

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

4.2 手寫AIO

import java.io.IOException;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.channels.AsynchronousServerSocketChannel;import java.nio.channels.AsynchronousSocketChannel;import java.nio.channels.CompletionHandler;public class AIOServer { public void startListen(int port) throws InterruptedException { try { AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(port)); serverSocketChannel.accept(null,new CompletionHandler() { @Override public void completed(AsynchronousSocketChannel socketChannel, Void attachment) { serverSocketChannel.accept(null,this); //收到連接后,應該調用accept方法等待新的連接進來 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); socketChannel.read(byteBuffer,byteBuffer, new CompletionHandler() { @Override public void completed(Integer num, ByteBuffer attachment) { if (num > 0){ attachment.flip(); System.out.println(new String(attachment.array()).trim()); }else { try { socketChannel.close(); } catch (IOException e) { e.printStackTrace(); } } } @Override public void failed(Throwable exc, ByteBuffer attachment) { System.out.println("read error"); exc.printStackTrace(); } }); } @Override public void failed(Throwable exc, Void attachment) { System.out.println("accept error"); exc.printStackTrace(); } }); } catch (IOException e) { e.printStackTrace(); }//模擬去做其他事情 while (true){ Thread.sleep(1000); } } public static void main(String[] args) throws InterruptedException { AIOServer aioServer = new AIOServer(); aioServer.startListen(8080); }}

五、面試金手指

5.1 BIO

BIO中是accept read write都是同步阻塞操作,這是無法改變,所有要改善bio,必須使用線程池,但是線程的創建和銷毀的代價比較大,特別是對于linux這種,一個線程就是一個進程的,但是沒辦法,只有這樣一種方式。

BIO中是accept read write都是同步阻塞操作:同步阻塞I/O模式,一個請求一個應答,數據的讀取寫入必須阻塞在一個線程內等待其完成。采用 BIO 通信模型 的服務端,通常由一個獨立的 Acceptor 線程負責監聽客戶端的連接。我們一般通過在 while(true) 循環中服務端會調用 accept() 方法等待接收客戶端的連接的方式監聽請求,請求一旦接收到一個連接請求,就可以建立通信套接字在這個通信套接字上進行讀寫操作,此時不能再接收其他客戶端連接請求,只能等待同當前連接的客戶端的操作執行完成, 不過可以通過多線程來支持多個客戶端的連接。

多線程,就是偽異步IO:如果要讓 BIO 通信模型 能夠同時處理多個客戶端請求,就必須使用多線程(主要原因是 socket.accept()、 socket.read()、 socket.write() 涉及的三個主要函數都是同步阻塞的),也就是說它在接收到客戶端連接請求之后為每個客戶端創建一個新的線程進行鏈路處理,處理完成之后,通過輸出流返回應答給客戶端,線程銷毀。這就是典型的 一請求一應答通信模型 。我們可以設想一下如果這個連接不做任何事情的話就會造成不必要的線程開銷,不過可以通過 線程池機制 改善,線程池還可以讓線程的創建和回收成本相對較低。使用FixedThreadPool 可以有效的控制了線程的最大數量,保證了系統有限的資源的控制,實現了N(客戶端請求數量):M(處理客戶端請求的線程數量)的偽異步I/O模型(N 可以遠遠大于 M),下面一節"偽異步 BIO"中會詳細介紹到。

多線程,偽異步IO的代價大,但是沒辦法:在 Java 虛擬機中,線程是寶貴的資源,線程的創建和銷毀成本很高,除此之外,線程的切換成本也是很高的。尤其在 Linux 這樣的操作系統中,線程本質上就是一個進程,創建和銷毀線程都是重量級的系統函數。如果并發訪問量增加會導致線程數急劇膨脹可能會導致線程堆棧溢出、創建新線程失敗等問題,最終導致進程宕機或者僵死,不能對外提供服務。

5.2 偽異步IO

線程池:為了解決同步阻塞I/O面臨的一個鏈路需要一個線程處理的問題,后來有人對它的線程模型進行了優化一一一后端通過一個線程池來處理多個客戶端的請求接入,形成客戶端個數M:線程池最大線程數N的比例關系,其中M可以遠遠大于N.通過線程池可以靈活地調配線程資源,設置線程的最大值,防止由于海量并發接入導致線程耗盡。

偽異步IO:采用線程池和任務隊列可以實現一種叫做偽異步的 I/O 通信框架,它的模型圖如上圖所示。當有新的客戶端接入時,將客戶端的 Socket 封裝成一個Task(該任務實現java.lang.Runnable接口,變成一個可以多線程的類)投遞到后端的線程池中進行處理,JDK 的線程池維護一個消息隊列和 N 個活躍線程,對消息隊列中的任務進行處理。由于線程池可以設置消息隊列的大小和最大線程數,因此,它的資源占用是可控的,無論多少個客戶端并發訪問,都不會導致資源的耗盡和宕機。

偽異步IO還是BIO:偽異步I/O通信框架采用了線程池實現,因此避免了為每個請求都創建一個獨立線程造成的線程資源耗盡問題。不過因為它的底層任然是同步阻塞的BIO模型,因此無法從根本上解決問題。

5.3 NIO

NIO中的N可以理解為Non-blocking,不單純是New。它支持面向緩沖的,基于通道的I/O操作方法。NIO提供了與傳統BIO模型中的 Socket 和 ServerSocket 相對應的 SocketChannel 和ServerSocketChannel

兩種不同的套接字通道實現,兩種通道都支持阻塞和非阻塞兩種模式。阻塞模式使用就像傳統中的支持一樣,比較簡單,但是性能和可靠性都不好;非阻塞模式正好與之相反。對于低負載、低并發的應用程序,可以使用同步阻塞I/O來提升開發速率和更好的維護性;對于高負載、高并發的(網絡)應用,應使用NIO 的非阻塞模式來開發。

5.4 NIO與IO的區別

問題:NIO與IO區別?

回答:首先,從 NIO 流是同步非阻塞,而 IO 流是同步阻塞 IO 說起;然后,從 NIO 的3個核心組件/特性為 NIO帶來的一些改進來分析。

第一,IO流是阻塞的,NIO流是不阻塞的

(1)Java IO的各種流是阻塞的。這意味著,當一個線程調用 accept() read() 或 write()時,該線程被阻塞,直到有一些數據被讀取,或數據完全寫入。該線程在此期間不能再干任何事情了。
(2)Java NIO使我們可以進行非阻塞IO操作。
非阻塞讀read:單線程中從通道讀取數據到buffer,同時可以繼續做別的事情,當數據讀取到buffer中后,線程再繼續處理數據。
非阻塞寫write:一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。

第二,NIO三要素Buffer(緩沖區) IO 面向流(Stream oriented),而 NIO 面向緩沖區(Buffer oriented)

Buffer是一個對象,它包含一些要寫入或者要讀出的數據。在NIO類庫中加入Buffer對象,體現了新庫與原I/O的一個重要區別。在面向流的I/O中,可以將數據直接寫入或者將數據直接讀到Stream**對象中。雖然 Stream 中也有 Buffer 開頭的擴展類,但只是流的包裝類,還是從流讀到緩沖區,而 NIO 卻是直接讀到 Buffer 中進行操作。
在NIO厙中,所有數據都是用緩沖區buffer處理的。在讀取數據時,它是直接讀到緩沖區中的;在寫入數據時,寫入到緩沖區中。任何時候訪問NIO中的數據,都是通過緩沖區進行操作。最常用的緩沖區是 ByteBuffer,一個 ByteBuffer 提供了一組功能用于操作 byte 數組。除了ByteBuffer,還有其他的一些緩沖區,事實上,每一種Java基本類型(除了Boolean類型)都對應有一種緩沖區。
值得注意的是,Buffer只對執行IO操作的線程有用(read/write),對執行連接的線程是沒有用的(accept)。

第三,NIO三要素Channel (通道)NIO 通過Channel(通道) 進行讀寫

通道是雙向的,可讀也可寫,而流的讀寫是單向的。無論讀寫,通道channel 只能和Buffer交互。因為 Buffer,通道可以異步地讀寫。

在這里,需要知道的是,NIO中的所有IO都是從 Channel(通道) 開始的。
從通道進行數據讀取 :創建一個緩沖區,然后請求通道讀取數據。
從通道進行數據寫入 :創建一個緩沖區,填充數據,并要求通道寫入數據。
如下圖所示:

第四,NIO三要素Selectors(選擇器) NIO有選擇器,而IO沒有

選擇器用于使用單個線程處理多個通道,因此,它需要較少的線程來處理這些通道。另一方面,線程之間的切換對于操作系統來說是昂貴的,因此,為了提高系統效率選擇器是有用的。操作過程中,Channel注冊到選擇器中,如下圖所示:

小結:從BIO到NIO,分下NIO三要素:Buffer Channel Selector
Buffer:表示滿了才讀,這就是linux五種IO中的同步非阻塞IO,即recvfrom輪詢;
Channel:ServerSocket 變為 ServerSocketChannel,Socket變為SocketChannel;
Selector:用來選擇Channel,所有的Channel都注冊在Selector上面。

5.5 AIO

異步 IO 是基于事件和回調機制實現的,也就是應用操作之后會直接返回,不會堵塞在那里,當后臺處理完成,操作系統會通知相應的線程進行后續的操作。
AIO 是異步IO的縮寫,雖然 NIO 在網絡操作中,提供了非阻塞的方法,但是 NIO 的 IO 行為還是同步的。對于 NIO 來說,我們的業務線程是在 IO 操作準備好時,得到通知,接著就由這個線程自行進行 IO 操作,IO操作本身是同步的。

小結:三種IO要完成的功能是一樣的:BIO也是accept read write三個操作,NIO也是accept read write三個操作,AIO也是accept read write三個操作,任何io都是accept read write三個操作。

六、尾聲

不斷升級,Java之BIO、NIO、AIO的演變,完成了。

天天打碼,天天進步!!!

如果覺得本文有用,可以關注+轉發,您的鼓勵就是我創作的最大動力。

總結

以上是生活随笔為你收集整理的bio linux 创建_不断升级,Java之BIO、NIO、AIO的演变的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。