确定不来了解一下什么是 BIO NIO AIO 阻塞 非阻塞 同步 异步?
本文內容涉及同步與異步, 阻塞與非阻塞, BIO、NIO、AIO等概念, 這塊內容本身比較復雜, 很難用三言兩語說明白. 而書上的定義更不容易理解是什么意思. 下面跟著我一起解開它們神秘的面紗。
- BIO 傳統的socket編程,屬于同步阻塞模型
- NIO 官方(new io) jdk1.4推出 俗稱(non-block io) ,屬于同步非阻塞模式
- AIO 又稱NIO2.0在jdk1.7推出,屬于異步非阻塞模式
解讀同步異步,阻塞非阻塞。
阻塞和非阻塞
從簡單的開始,我們以經典的讀取文件的模型舉例。(對操作系統而言,所有的輸入輸出設備都被抽象成文件。)
在發起讀取文件的請求時,應用層會調用系統內核的I/O接口。
如果應用層調用的是阻塞型I/O,那么在調用之后,應用層即刻被掛起,一直出于等待數據返回的狀態,直到系統內核從磁盤讀取完數據并返回給應用層,應用層才用獲得的數據進行接下來的其他操作。
如果應用層調用的是非阻塞I/O,那么調用后,系統內核會立即返回(雖然還沒有文件內容的數據),應用層并不會被掛起,它可以做其他任意它想做的操作。(至于文件內容數據如何返回給應用層,這已經超出了阻塞和非阻塞的辨別范疇。)
這便是(脫離同步和異步來說之后)阻塞和非阻塞的區別。總結來說,是否是阻塞還是非阻塞,關注的是接口調用(發出請求)后等待數據返回時的狀態。被掛起無法執行其他操作的則是阻塞型的,可以被立即「抽離」去完成其他「任務」的則是非阻塞型的。
同步和異步
阻塞和非阻塞解決了應用層等待數據返回時的狀態問題,那系統內核獲取到的數據到底如何返回給應用層呢?這里不同類型的操作便體現的是同步和異步的區別。
對于同步型的調用,應用層需要自己去向系統內核問詢,如果數據還未讀取完畢,那此時讀取文件的任務還未完成,應用層根據其阻塞和非阻塞的劃分,或掛起或去做其他事情(所以同步和異步并不決定其等待數據返回時的狀態);如果數據已經讀取完畢,那此時系統內核將數據返回給應用層,應用層即可以用取得的數據做其他相關的事情。
而對于異步型的調用,應用層無需主動向系統內核問詢,在系統內核讀取完文件數據之后,會主動通知應用層數據已經讀取完畢,此時應用層即可以接收系統內核返回過來的數據,再做其他事情。
這便是(脫離阻塞和非阻塞來說之后)同步和異步的區別。也就是說,是否是同步還是異步,關注的是任務完成時消息通知的方式。由調用方盲目主動問詢的方式是同步調用,由被調用方主動通知調用方任務已完成的方式是異步調用。
假設小明需要在網上下載一個軟件:
如果小明點擊下載按鈕之后,就一直干瞪著進度條不做其他任何事情直到軟件下載完成,這是同步阻塞; 如果小明點擊下載按鈕之后,就一直干瞪著進度條不做其他任何事情直到軟件下載完成,但是軟件下載完成其實是會「叮」的一聲通知的(但小明依然那樣干等著),這是異步阻塞;(不常見) 如果小明點擊下載按鈕之后,就去做其他事情了,不過他總需要時不時瞄一眼屏幕看軟件是不是下載完成了,這是同步非阻塞; 如果小明點擊下載按鈕之后,就去做其他事情了,軟件下載完之后「叮」的一聲通知小明,小明再回來繼續處理下載完的軟件,這是異步非阻塞。 相信看完以上這個案例之后,這幾個概念已經能夠分辨得很清楚了。
總的來說,同步和異步關注的是任務完成消息通知的機制,而阻塞和非阻塞關注的是等待任務完成時請求者的狀態。
java網絡編程
我們通過 客戶端像服務端查詢信息作為一個例子。分別通過三種模型來實現。
1.1、傳統的BIO
在傳統的網絡編程中,服務端監聽端口,客戶端請求服務端的ip跟監聽的端口,跟服務端通信,必須三次握手建立。如果連接成功,通過套接字(socket)進行通信。 在BIO通信模型:采用BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監聽客戶端的連接,它接收到客戶端連接請求之后為每個客戶端創建一個新的線程進行鏈路處理沒處理完成后,通過輸出流返回應答給客戶端,線程銷毀。即典型的一請求一應答通信模型。
從圖中可以得知,該模型中每一個請求對應一個線程處理,在線程數量有限的情況下,請求數量多,那么服務器就會因為資源不足而掛掉。 服務端代碼 /*** @author yukong* @date 2018年8月24日18:51:40* 服務端*/ public class Server {/*** 默認端口*/private static final Integer DEFAULT_PORT = 6789;public void start() throws IOException {start(DEFAULT_PORT);}public void start(Integer port) throws IOException {ServerSocket serverSocket = new ServerSocket(port);System.out.println("小yu機器人啟動,監聽端口為:" + port);//通過無線循環監聽客戶端連接while (true) {// 阻塞方法,直至有客戶端連接成功Socket socket = serverSocket.accept();// 多線程處理客戶端請求new Thread(new ServerHandler(socket)).start();}}public static void main(String[] args) throws IOException {new Server().start();}} 復制代碼服務端處理器代碼
/*** @author yukong* @date 2018年8月24日18:51:40* 服務端業務邏輯處理器*/ public class ServerHandler implements Runnable{private Socket socket;public ServerHandler(Socket socket) {this.socket = socket;}@Overridepublic void run() {BufferedReader in = null;PrintWriter out = null;try {// 獲取socket的字符緩存輸入流 也就是獲取客戶端給服務器的字符流in = new BufferedReader( new InputStreamReader(this.socket.getInputStream()));// 獲取socket的字符輸出流 也就是發送的客戶的字符流 第二個參數自動刷新out = new PrintWriter( new OutputStreamWriter(this.socket.getOutputStream()), true);String request, response;// 讀取輸入流的消息 如果為空 則退出讀取while ((request = in.readLine()) != null) {System.out.println("[" + Thread.currentThread().getName()+ "]" + "小yu機器人收到消息:" + request);// 具體業務邏輯處理 查詢信息。response = ResponseUtil.queryMessage(request);out.println(response);}} catch (IOException e) {e.printStackTrace();} finally {// 資源釋放if (in != null) {try {in.close();} catch (IOException e) {e.printStackTrace();}in = null;}if (out != null) {out.close();out = null;}if (socket != null) {try {socket.close();} catch (IOException e) {e.printStackTrace();}socket = null;}}}} 復制代碼客戶端代碼
/*** @author yukong* @date 2018年8月24日18:51:40* 客戶端*/ public class Client {/*** 默認端口*/private static final Integer DEFAULT_PORT = 6789;/*** 默認端口*/private static final String DEFAULT_HOST = "localhost";public void send(String key){send(DEFAULT_PORT,key);}public void send(int port,String key){System.out.println("查詢的key為:" + key);Socket socket = null;BufferedReader in = null;PrintWriter out = null;try{socket = new Socket(DEFAULT_HOST,port);in = new BufferedReader(new InputStreamReader(socket.getInputStream()));out = new PrintWriter(socket.getOutputStream(),true);out.println(key);System.out.println("查詢的結果為:" + in.readLine());}catch(Exception e){e.printStackTrace();}finally{if(in != null){try {in.close();} catch (IOException e) {e.printStackTrace();}in = null;}if(out != null){out.close();out = null;}if(socket != null){try {socket.close();} catch (IOException e) {e.printStackTrace();}socket = null;}}}public static void main(String[] args) {Scanner scanner = new Scanner(System.in);Client client = new Client();while (scanner.hasNext()) {String key = scanner.next();client.send(key);}}} 復制代碼從代碼中可以得知,我們每次請求都是new Thread去處理,意味著線程消耗巨大,可能會有朋友說道,那就用線程池,同樣的如果使用線程池,當達到線程最大數量,也會達到瓶頸。該模式不適合高并發的訪問。
1.2 NIO模型
NIO提供了與傳統BIO模型中的Socket和ServerSocket相對應的SocketChannel和ServerSocketChannel兩種不同的套接字通道實現。 ? 新增的著兩種通道都支持阻塞和非阻塞兩種模式。 阻塞模式使用就像傳統中的支持一樣,比較簡單,但是性能和可靠性都不好;非阻塞模式正好與之相反。 對于低負載、低并發的應用程序,可以使用同步阻塞I/O來提升開發速率和更好的維護性;對于高負載、高并發的(網絡)應用,應使用NIO的非阻塞模式來開發。 下面會先對基礎知識進行介紹。
1.2.1、緩沖區 Buffer
Buffer是一個對象,包含一些要寫入或者讀出的數據。
在NIO庫中,所有數據都是用緩沖區處理的。在讀取數據時,它是直接讀到緩沖區中的;在寫入數據時,也是寫入到緩沖區中。任何時候訪問NIO中的數據,都是通過緩沖區進行操作。 緩沖區實際上是一個數組,并提供了對數據結構化訪問以及維護讀寫位置等信息。 ? ?具體的緩存區有這些:ByteBuffe、CharBuffer、 ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。他們實現了相同的接口:Buffer。
1.2.2、通道 Channel
我們對數據的讀取和寫入要通過Channel,它就像水管一樣,是一個通道。通道不同于流的地方就是通道是雙向的,可以用于讀、寫和同時讀寫操作。 ? 底層的操作系統的通道一般都是全雙工的,所以全雙工的Channel比流能更好的映射底層操作系統的API。 Channel主要分兩大類:
- SelectableChannel:用戶網絡讀寫
- FileChannel:用于文件操作
后面代碼會涉及的ServerSocketChannel和SocketChannel都是SelectableChannel的子類。
1. 2.3、多路復用器 Selector
Selector是Java ?NIO 編程的基礎。 Selector提供選擇已經就緒的任務的能力:Selector會不斷輪詢注冊在其上的Channel,如果某個Channel上面發生讀或者寫事件,這個Channel就處于就緒狀態,會被Selector輪詢出來,然后通過SelectionKey可以獲取就緒Channel的集合,進行后續的I/O操作。 一個Selector可以同時輪詢多個Channel,因為JDK使用了epoll()代替傳統的select實現,所以沒有最大連接句柄1024/2048的限制。所以,只需要一個線程負責Selector的輪詢,就可以接入成千上萬的客戶端。 服務端代碼
/*** 服務端*/ public class Server {/*** 默認端口*/private static final Integer DEFAULT_PORT = 6780;public void start() throws IOException {start(DEFAULT_PORT);}public void start(Integer port) throws IOException {// 打開多路復用選擇器Selector selector = Selector.open();// 打開服務端監聽通道ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();// 設置為非阻塞模式serverSocketChannel.configureBlocking(false);// 綁定監聽的端口serverSocketChannel.bind(new InetSocketAddress(port));// 將選擇器綁定到監聽信道,只有非阻塞信道才可以注冊選擇器.并在注冊過程中指出該信道可以進行Accept操作serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("小yu機器人啟動,監聽端口為:" + port);new Thread(new ServerHandler(selector)).start();}public static void main(String[] args) throws IOException {new Server().start();}} 復制代碼服務端處理器
public class ServerHandler implements Runnable{private Selector selector;public ServerHandler(Selector selector) {this.selector = selector;}public void run() {try {while (true) {// 等待某信道就緒(或超時)if(selector.select(1000)==0){ // System.out.print("獨自等待.");continue;}// 取得迭代器.selectedKeys()中包含了每個準備好某一I/O操作的信道的SelectionKeyIterator<SelectionKey> keyIterator=selector.selectedKeys().iterator();while (keyIterator.hasNext()){SelectionKey sk = keyIterator.next();// 刪除已選的key 以防重負處理keyIterator.remove();// 處理keyhandlerSelect(sk);}}} catch (IOException e) {}}private void handlerSelect(SelectionKey sk) throws IOException {// 處理新接入的請求if (sk.isAcceptable()) {ServerSocketChannel ssc = (ServerSocketChannel) sk.channel();//通過ServerSocketChannel的accept創建SocketChannel實例//完成該操作意味著完成TCP三次握手,TCP物理鏈路正式建立SocketChannel sc = ssc.accept();//設置為非阻塞的sc.configureBlocking(false);//注冊為讀sc.register(selector, SelectionKey.OP_READ);}// 讀操作if (sk.isReadable()) {String request, response;SocketChannel sc = (SocketChannel) sk.channel();// 創建一個ByteBuffer 并設置大小為1mByteBuffer byteBuffer = ByteBuffer.allocate(1024);// 獲取到讀取的字節長度int readBytes = sc.read(byteBuffer);// 判斷是否有數據if (readBytes > 0) {//將緩沖區當前的limit設置為position=0,用于后續對緩沖區的讀取操作byteBuffer.flip();//根據緩沖區可讀字節數創建字節數組byte[] bytes = new byte[byteBuffer.remaining()];// 復制至新的緩沖字節流byteBuffer.get(bytes);request = new String(bytes, "UTF-8");System.out.println("[" + Thread.currentThread().getName()+ "]" + "小yu機器人收到消息:" + request);// 具體業務邏輯處理 查詢信息。response = ResponseUtil.queryMessage(request);//將消息編碼為字節數組byte[] responseBytes = response.getBytes();//根據數組容量創建ByteBufferByteBuffer writeBuffer = ByteBuffer.allocate(responseBytes.length);//將字節數組復制到緩沖區writeBuffer.put(responseBytes);//flip操作writeBuffer.flip();//發送緩沖區的字節數組sc.write(writeBuffer);}}}} 復制代碼客戶端
/*** 客戶端*/ public class Client {// 通道選擇器private Selector selector;// 與服務器通信的通道SocketChannel socketChannel;/*** 默認端口*/private static final Integer DEFAULT_PORT = 6780;/*** 默認端口*/private static final String DEFAULT_HOST = "127.0.0.1";public void send(String key) throws IOException {send(DEFAULT_PORT, DEFAULT_HOST, key);}public void send(int port,String host, String key) throws IOException {init(port, host);System.out.println("查詢的key為:" + key);//將消息編碼為字節數組byte[] bytes = key.getBytes();//根據數組容量創建ByteBufferByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);//將字節數組復制到緩沖區writeBuffer.put(bytes);//flip操作writeBuffer.flip();//發送緩沖區的字節數組socketChannel.write(writeBuffer);//****此處不含處理“寫半包”的代碼}public void init(int port,String host) throws IOException {// 創建選擇器selector = Selector.open();// 設置鏈接的服務端地址InetSocketAddress socketAddress = new InetSocketAddress(host, port);// 打開通道socketChannel = SocketChannel.open(socketAddress);// 非阻塞socketChannel.configureBlocking(false);socketChannel.register(selector, SelectionKey.OP_READ);new Thread(new ClientHandler(selector)).start();}public static void main(String[] args) throws IOException {Scanner scanner = new Scanner(System.in);Client client = new Client();while (scanner.hasNext()) {String key = scanner.next();client.send(key);}}} 復制代碼客戶端處理器
public class ClientHandler implements Runnable {private Selector selector;public ClientHandler(Selector selector) {this.selector = selector;}public void run() {try {while (true) {if(selector.select(1000) < 0) {continue;}Set<SelectionKey> keys = selector.selectedKeys();Iterator<SelectionKey> selectionKeyIterator = keys.iterator();while (selectionKeyIterator.hasNext()) {SelectionKey sc = selectionKeyIterator.next();selectionKeyIterator.remove();//讀消息if (sc.isReadable()) {SocketChannel socketChannel = (SocketChannel) sc.channel();//創建ByteBuffer,并開辟一個1M的緩沖區ByteBuffer byteBuffer = ByteBuffer.allocate(1024);//讀取請求碼流,返回讀取到的字節數int byteSize = socketChannel.read(byteBuffer);if (byteSize > 0) {//將緩沖區當前的limit設置為position=0,用于后續對緩沖區的讀取操作byteBuffer.flip();//根據緩沖區可讀字節數創建字節數組 總長度減去空余的byte[] bytes = new byte[byteBuffer.remaining()];// 復制至新的緩沖字節流byteBuffer.get(bytes);String message = new String(bytes, "UTF-8");System.out.println(message);}}}}} catch (IOException e) {}} }復制代碼從代碼中 我們也能看出來,nio解決的是阻塞與非阻塞的,通過selector輪詢上注冊的channel的狀態,來獲取對應準備就緒channel的 那么請求者就不用一直去accpet阻塞,等待了。那為什么是同步呢,因為還是我們請求者不停的輪詢selector是否有完全就緒的channel。
3、AIO編程
NIO 2.0引入了新的異步通道的概念,并提供了異步文件通道和異步套接字通道的實現。 異步的套接字通道時真正的異步非阻塞I/O,對應于UNIX網絡編程中的事件驅動I/O(AIO)。他不需要過多的Selector對注冊的通道進行輪詢即可實現異步讀寫,從而簡化了NIO的編程模型。 服務端代碼
/*** 異步非阻塞服務端*/ public class Sever {/*** 默認端口*/private static final Integer DEFAULT_PORT = 6780;private AsynchronousServerSocketChannel serverChannel;//作為handler接收客戶端連接class ServerCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, Void> {private AsynchronousServerSocketChannel serverChannel;private ByteBuffer buffer = ByteBuffer.allocateDirect(1024);private CharBuffer charBuffer;private CharsetDecoder decoder = Charset.defaultCharset().newDecoder();public ServerCompletionHandler(AsynchronousServerSocketChannel serverChannel) {this.serverChannel = serverChannel;}public void completed(AsynchronousSocketChannel result, Void attachment) {//立即接收下一個請求,不停頓serverChannel.accept(null, this);try {while (result.read(buffer).get() != -1) {buffer.flip();charBuffer = decoder.decode(buffer);String request = charBuffer.toString().trim();System.out.println("[" + Thread.currentThread().getName()+ "]" + "小yu機器人收到消息:" + request);// 具體業務邏輯處理 查詢信息。String response = ResponseUtil.queryMessage(request);//將消息編碼為字節數組byte[] responseBytes = response.getBytes();//根據數組容量創建ByteBufferByteBuffer outBuffer = ByteBuffer.allocate(responseBytes.length);//將字節數組復制到緩沖區outBuffer.put(responseBytes);//flip操作outBuffer.flip();//發送緩沖區的字節數組result.write(outBuffer).get();if (buffer.hasRemaining()) {buffer.compact();} else {buffer.clear();}}} catch (InterruptedException | ExecutionException e) {e.printStackTrace();} catch (CharacterCodingException e) {e.printStackTrace();} finally {try {result.close();} catch (IOException e) {e.printStackTrace();}}}public void failed(Throwable exc, Void attachment) {//立即接收下一個請求,不停頓serverChannel.accept(null, this);throw new RuntimeException("connection failed!");}}public void init() throws IOException, InterruptedException {init(DEFAULT_PORT);}public void init(Integer port) throws IOException, InterruptedException {// 打開異步通道this.serverChannel = AsynchronousServerSocketChannel.open();// 判斷通道是否打開if (serverChannel.isOpen()) {serverChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);serverChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);serverChannel.bind(new InetSocketAddress(port));} else {throw new RuntimeException("Channel not opened!");}start(port);}public void start(Integer port) throws InterruptedException {System.out.println("小yu機器人啟動,監聽端口為:" + port);this.serverChannel.accept(null, new ServerCompletionHandler(serverChannel));// 保證線程不會掛了while (true) {Thread.sleep(5000);}}public static void main(String[] args) throws IOException, InterruptedException {Sever server = new Sever();server.init();} }復制代碼客戶端
public class Client {class ClientCompletionHandler implements CompletionHandler<Void, Void> {private AsynchronousSocketChannel channel;private CharBuffer charBufferr = null;private CharsetDecoder decoder = Charset.defaultCharset().newDecoder();private BufferedReader clientInput = new BufferedReader(new InputStreamReader(System.in));public ClientCompletionHandler(AsynchronousSocketChannel channel) {this.channel = channel;}public void completed(Void result, Void attachment) {System.out.println("Input Client Reuest:");String request;try {request = clientInput.readLine();channel.write(ByteBuffer.wrap(request.getBytes()));ByteBuffer buffer = ByteBuffer.allocateDirect(1024);while(channel.read(buffer).get() != -1){buffer.flip();charBufferr = decoder.decode(buffer);System.out.println(charBufferr.toString());if(buffer.hasRemaining()){buffer.compact();}else{buffer.clear();}request = clientInput.readLine();channel.write(ByteBuffer.wrap(request.getBytes())).get();}} catch (IOException e) {e.printStackTrace();} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}finally {try {channel.close();} catch (IOException e) {e.printStackTrace();}}}public void failed(Throwable exc, Void attachment) {throw new RuntimeException("channel not opened!");}}public void start() throws IOException, InterruptedException{AsynchronousSocketChannel channel = AsynchronousSocketChannel.open();if(channel.isOpen()){channel.setOption(StandardSocketOptions.SO_RCVBUF, 128*1024);channel.setOption(StandardSocketOptions.SO_SNDBUF, 128*1024);channel.setOption(StandardSocketOptions.SO_KEEPALIVE,true);channel.connect(new InetSocketAddress("127.0.0.1",6780),null,new ClientCompletionHandler(channel));while(true){Thread.sleep(5000);}}else{throw new RuntimeException("Channel not opened!");}}public static void main(String[] args) throws IOException, InterruptedException{Client client = new Client();client.start();}} 復制代碼4、各種I/O的對比
先以一張表來直觀的對比一下:
5 References
完全理解同步/異步與阻塞/非阻塞
Java 網絡IO編程總結
完整代碼地址
轉載于:https://juejin.im/post/5d01ab7e6fb9a07ecf721f46
總結
以上是生活随笔為你收集整理的确定不来了解一下什么是 BIO NIO AIO 阻塞 非阻塞 同步 异步?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 「原创」猪肚子鸡的做法
- 下一篇: 卡旺达户外电源一物在手 卡旺达户外电源怎