到如今爲止,Java IO可分爲三類:BIO、NIO、AIO。最先出現的是BIO,而後是NIO,最近的是AIO,BIO即Blocking IO,NIO有的文章說是New NIO,也有的文章說是No Blocking IO,我查了一些資料,官網說的應該是No Blocking IO,提供了Selector,Channle,SelectionKey抽象,AIO即Asynchronous IO(異步IO),提供了Fauture等異步操做。java
[](https://imgchr.com/i/i81QZn)編程
[數組
](https://imgchr.com/i/i81QZn)緩存
上圖是BIO的架構體系圖。能夠看到BIO主要分爲兩類IO,即字符流IO和字節流IO,字符流即把輸入輸出數據當作字符來看待,Writer和Reader是其繼承體系的最高層,字節流即把輸入輸出當作字節來看待,InputStream和OutputStream是其繼承體系的最高層。下面以文件操做爲例,其餘的實現類也很是相似。服務器
順便說一下,整個BIO體系大量使用了裝飾者模式,例如BufferedInputStream包裝了InputStream,使其擁有了緩衝的能力。
public class Main { public static void main(String[] args) throws IOException { //寫入文件 FileOutputStream out = new FileOutputStream("E:Java_projecteffective-javasrctopyeononiobiotest.txt"); out.write("hello,world".getBytes("UTF-8")); out.flush(); out.close(); //讀取文件 FileInputStream in = new FileInputStream("E:Java_projecteffective-javasrctopyeononiobiotest.txt"); byte[] buffer = new byte[in.available()]; in.read(buffer); System.out.println(new String(buffer, "UTF-8")); in.close(); } } 複製代碼
向FileOutputStream構造函數中傳入文件名來建立FileOutputStream對象,即打開了一個字節流,以後使用write方法向字節流中寫入數據,完成以後調用flush刷新緩衝區,最後記得要關閉字節流。讀取文件也是相似的,先打開一個字節流,而後從字節流中讀取數據並存入內存中(buffer數組),而後再關閉字節流。架構
由於InputStream和OutputStream都繼承了AutoCloseable接口,因此若是使用的是try-resource的語法來進行字節流的IO操做,可不須要手動顯式調用close方法了,這也是很是推薦的作法,在示例中我沒有這樣作只是爲了方便。
字節流主要使用的是InputStream和OutputStream,而字符流主要使用的就是與之對應的Reader和Writer。下面來看一個示例,該示例的功能和上述示例的同樣,只不過實現手段不一樣:框架
public class Main { public static void main(String[] args) throws IOException { Writer writer = new FileWriter("E:Java_projecteffective-javasrctopyeononiobiotest.txt"); writer.write("hello,worldn"); writer.write("hello,yeononn"); writer.flush(); writer.close(); BufferedReader reader = new BufferedReader(new FileReader("E:Java_projecteffective-javasrctopyeononiobiotest.txt")); String line = ""; int lineCount = 0; while ((line = reader.readLine()) != null) { System.out.println(line); lineCount++; } reader.close(); System.out.println(lineCount); } } 複製代碼
Writer很是簡單,沒法就是打開字符流,而後向字符流中寫入字符,而後關閉。關鍵是Reader,示例代碼中使用了BufferedReader來包裝FileReader,使得本來沒有緩衝功能的FileReader有了緩衝功能,這就是上面提到過的裝飾者模式,BufferedReader還提供了方便使用的API,例如readLine(),這個方法每次調用會讀取文件中的一行。異步
以上就是BIO的簡單使用,源碼的話由於涉及太多的底層,因此若是對底層不是很瞭解的話會很難理解源碼。socket
BIO是同步阻塞的IO,而NIO是同步非阻塞的IO。NIO中有幾個比較重要的組件:Selector,SelectionKey,Channel,ByteBuffer,其中Selector就是所謂的選擇器,SelectionKey能夠簡單理解爲選擇鍵,這個鍵將Selector和Channle進行一個綁定(或者所Channle註冊到Selector上),當有數據到達Channel的時候,Selector會從阻塞狀態中恢復過來,並對該Channle進行操做,而且,咱們不能直接對Channle進行讀寫操做,只能對ByteBuffer操做。以下圖所示:
[](https://imgchr.com/i/i8YB80)
[
](https://imgchr.com/i/i8YB80)
下面是一個Socket網絡編程的例子:
//服務端 public class SocketServer { private Selector selector; private final static int port = 9000; private final static int BUF = 10240; private void init() throws IOException { //獲取一個Selector selector = Selector.open(); //獲取一個服務端socket Channel ServerSocketChannel channel = ServerSocketChannel.open(); //設置爲非阻塞模式 channel.configureBlocking(false); //綁定端口 channel.socket().bind(new InetSocketAddress(port)); //把channle註冊到Selector上,並表示對ACCEPT事件感興趣 SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_ACCEPT); while (true) { //該方法會阻塞,直到和其綁定的任何一個channel有數據過來 selector.select(); //獲取該Selector綁定的SelectionKey Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); //記得刪除,不然就無限循環了 iterator.remove(); //若是該事件是一個ACCEPT,那麼就執行doAccept方法,其餘的也同樣 if (key.isAcceptable()) { doAccept(key); } else if (key.isReadable()) { doRead(key); } else if (key.isWritable()) { doWrite(key); } else if (key.isConnectable()) { System.out.println("鏈接成功!"); } } } } //寫方法,注意不能直接對channle進行讀寫操做,只能對ByteBuffer進行操做 private void doWrite(SelectionKey key) throws IOException { ByteBuffer buffer = ByteBuffer.allocate(BUF); buffer.flip(); SocketChannel socketChannel = (SocketChannel) key.channel(); while (buffer.hasRemaining()) { socketChannel.write(buffer); } buffer.compact(); } //讀取消息 private void doRead(SelectionKey key) throws IOException { SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(BUF); long reads = socketChannel.read(buffer); while (reads > 0) { buffer.flip(); byte[] data = buffer.array(); System.out.println("讀取到消息: " + new String(data, "UTF-8")); buffer.clear(); reads = socketChannel.read(buffer); } if (reads == -1) { socketChannel.close(); } } //當有鏈接過來的時候,獲取鏈接過來的channle,而後註冊到Selector上,並設置成對讀消息感興趣,當客戶端有消息過來的時候,Selector就可讓其執行doRead方法,而後讀取消息並打印。 private void doAccept(SelectionKey key) throws IOException { ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel(); System.out.println("服務端監聽中..."); SocketChannel socketChannel = serverChannel.accept(); socketChannel.configureBlocking(false); socketChannel.register(key.selector(), SelectionKey.OP_READ); } public static void main(String[] args) throws IOException { SocketServer server = new SocketServer(); server.init(); } } //客戶端,寫得比較簡單 public class SocketClient { private final static int port = 9000; private final static int BUF = 10240; private void init() throws IOException { //獲取channel SocketChannel channel = SocketChannel.open(); //鏈接到遠程服務器 channel.connect(new InetSocketAddress(port)); //設置非阻塞模式 channel.configureBlocking(false); //往ByteBuffer裏寫消息 ByteBuffer buffer = ByteBuffer.allocate(BUF); buffer.put("Hello,Server".getBytes("UTF-8")); buffer.flip(); //將ByteBuffer內容寫入Channle,即發送消息 channel.write(buffer); channel.close(); } public static void main(String[] args) throws IOException { SocketClient client = new SocketClient(); client.init(); } } 複製代碼
嘗試啓動一個服務端,多個客戶端,結果大體以下所示:
服務端監聽中... 讀取到消息: Hello,Server 服務端監聽中... 讀取到消息: Hello,Server 複製代碼
註釋寫得挺清楚了,我這裏只是簡單使用了NIO,但實際上NIO遠遠不止這些東西,光一個ByteBuffer就能說一天,若是有機會,我會在後面Netty相關的文章中詳細說一下這幾個組件。在此就再也不多說了。
吐槽一些,純NIO寫的服務端和客戶端實在是太麻煩了,一不當心就會寫錯,仍是使用Netty相似的框架好一些啊。
在JDK7中新增了一些IO相關的API,這些API稱做AIO。由於其提供了一些異步操做IO的功能,但本質是其實仍是NIO,因此能夠簡單的理解爲是NIO的擴充。AIO中最重要的就是Future了,Future表示未來的意思,即這個操做可能會持續很長時間,但我不會等,而是到未來操做完成的時候,再過來通知我,這就是異步的意思。下面是兩個使用AIO的例子:
public static void main(String[] args) throws ExecutionException, InterruptedException, IOException { Path path = Paths.get("E:Java_projecteffective-javasrctopyeononioaiotest.txt"); AsynchronousFileChannel channel = AsynchronousFileChannel.open(path); ByteBuffer buffer = ByteBuffer.allocate(1024); Future<Integer> future = channel.read(buffer,0); Integer readNum = future.get(); //阻塞,若是不調用該方法,main方法會繼續執行 buffer.flip(); System.out.println(new String(buffer.array(), "UTF-8")); System.out.println(readNum); } 複製代碼
第一個例子使用AsynchronousFileChannel來異步的讀取文件內容,在代碼中,我使用了future.get()方法,該方法會阻塞當前線程,在例子中即主線程,當工做線程,即讀取文件的線程執行完畢後纔會從阻塞狀態中恢復過來,並將結果返回。以後就能夠從ByteBuffer中讀取數據了。這是使用未來時的例子,下面來看看使用回調的例子:
public class Main { public static void main(String[] args) throws ExecutionException, InterruptedException, IOException { Path path = Paths.get("E:Java_projecteffective-javasrctopyeononioaiotest.txt"); AsynchronousFileChannel channel = AsynchronousFileChannel.open(path); ByteBuffer buffer = ByteBuffer.allocate(1024); channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer result, ByteBuffer attachment) { System.out.println("完成讀取"); try { System.out.println(new String(attachment.array(), "UTF-8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } @Override public void failed(Throwable exc, ByteBuffer attachment) { System.out.println("讀取失敗"); } }); System.out.println("繼續執行主線程"); //調用完成以後不須要等待任務完成,會直接繼續執行主線程 while (true) { Thread.sleep(1000); } } } 複製代碼
輸出的結果大體以下所示,但不必定,這取決於線程調度:
繼續執行主線程 完成讀取 hello,world hello,yeonon 複製代碼
當任務完成,即讀取文件完畢的時候,會調用completed方法,失敗會調用failed方法,這就是回調。詳細接觸過回調的朋友應該不難理解。
以上就是我理解的BIO、NIO和AIO區別。
本文簡單粗略的講了一下BIO、NIO、AIO的使用,並未涉及源碼,也沒有涉及太多的原理,若是讀者但願瞭解更多關於三者的內容,建議參看一些書籍,例如老外寫的《Java NIO》,該書全面系統的講解了NIO的各類組件和細節,很是推薦。
原文連接:https://juejin.cn/post/684490...來源:掘金