到如今爲止,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
上圖是BIO的架構體系圖。能夠看到BIO主要分爲兩類IO,即字符流IO和字節流IO,字符流即把輸入輸出數據當作字符來看待,Writer和Reader是其繼承體系的最高層,字節流即把輸入輸出當作字節來看待,InputStream和OutputStream是其繼承體系的最高層。下面以文件操做爲例,其餘的實現類也很是相似。shell
順便說一下,整個BIO體系大量使用了裝飾者模式,例如BufferedInputStream包裝了InputStream,使其擁有了緩衝的能力。編程
public class Main {
public static void main(String[] args) throws IOException {
//寫入文件
FileOutputStream out = new FileOutputStream("E:\\Java_project\\effective-java\\src\\top\\yeonon\\io\\bio\\test.txt");
out.write("hello,world".getBytes("UTF-8"));
out.flush();
out.close();
//讀取文件
FileInputStream in = new FileInputStream("E:\\Java_project\\effective-java\\src\\top\\yeonon\\io\\bio\\test.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_project\\effective-java\\src\\top\\yeonon\\io\\bio\\test.txt");
writer.write("hello,world\n");
writer.write("hello,yeonon\n");
writer.flush();
writer.close();
BufferedReader reader = new BufferedReader(new FileReader("E:\\Java_project\\effective-java\\src\\top\\yeonon\\io\\bio\\test.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的簡單使用,源碼的話由於涉及太多的底層,因此若是對底層不是很瞭解的話會很難理解源碼。架構
BIO是同步阻塞的IO,而NIO是同步非阻塞的IO。NIO中有幾個比較重要的組件:Selector,SelectionKey,Channel,ByteBuffer,其中Selector就是所謂的選擇器,SelectionKey能夠簡單理解爲選擇鍵,這個鍵將Selector和Channle進行一個綁定(或者所Channle註冊到Selector上),當有數據到達Channel的時候,Selector會從阻塞狀態中恢復過來,並對該Channle進行操做,而且,咱們不能直接對Channle進行讀寫操做,只能對ByteBuffer操做。以下圖所示:框架
下面是一個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_project\\effective-java\\src\\top\\yeonon\\io\\aio\\test.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_project\\effective-java\\src\\top\\yeonon\\io\\aio\\test.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的各類組件和細節,很是推薦。