Java SE基礎鞏固(六):Java IO
到現在為止,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等非同步操作。
1 BIO
上圖是BIO的架構體系圖。可以看到BIO主要分為兩類IO,即字元流IO和位元組流IO,字元流即把輸入輸出資料當做字元來看待,Writer和Reader是其繼承體系的最高層,位元組流即把輸入輸出當做位元組來看待,InputStream和OutputStream是其繼承體系的最高層。下面以檔案操作為例,其他的實現類也非常類似。
順便說一下,整個BIO體系大量使用了裝飾者模式,例如BufferedInputStream包裝了InputStream,使其擁有了緩衝的能力。
1.1 位元組流
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方法了,這也是非常推薦的做法,在示例中我沒有這樣做只是為了方便。
1.2 字元流
位元組流主要使用的是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的簡單使用,原始碼的話因為涉及太多的底層,所以如果對底層不是很瞭解的話會很難理解原始碼。
2 NIO
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類似的框架好一些啊。
3 AIO
在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,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方法,這就是回撥。詳細接觸過回撥的朋友應該不難理解。
4 BIO、NIO、AIO的區別
- BIO是同步阻塞的IO,NIO是同步非阻塞IO,AIO非同步非阻塞IO,這是最基本的區別。阻塞模式會導致其他執行緒被IO執行緒阻塞,必須等待IO執行緒執行完畢才能繼續執行邏輯,非阻塞和非同步並不等同,非阻塞模式下,一般會採用事件輪詢的方式來執行IO,即IO多路複用,雖然仍然是同步的,但執行效率比傳統的BIO要高很多,AIO則是非同步IO,如果把IO工作當做一個任務的話,在當前執行緒中提交一個任務之後,不會有阻塞,會繼續執行當前執行緒的後續邏輯,在任務完成之後,當前執行緒會收到通知,然後再決定如何處理,這種方式的IO,CPU效率是最高的,CPU幾乎沒有發生過停頓,而時一直至於忙狀態,所以效率非常高,但程式設計難度會比較大。
- BIO面向的是流,無論是字元流還是位元組流,通俗的講,BIO在讀寫資料的時候會按照一個接一個的方式讀寫,而NIO和AIO(因為AIO實際上是NIO的擴充,所以從這個方面來看,可以把他們放在一塊)讀寫資料的時候是按照一塊一塊的讀取的,讀取到的資料會快取在記憶體中,然後在記憶體中對資料進行處理,這種方式的好處是減少了硬碟或者網路的讀寫次數,從而降低了由於硬碟或網路速度慢帶來的效率影響。
- BIO的API雖然比較底層,但如果熟悉之後編寫起來會比較容易,NIO或者AIO的API抽象層次高,一般來說應該更容易使用才是,但實際上卻很難“正確”的編寫,而且DEBUG的難度也較大,這也是為什麼Netty等NIO框架受歡迎的原因之一。
以上就是我理解的BIO、NIO和AIO區別。
5 小結
本文簡單粗略的講了一下BIO、NIO、AIO的使用,並未涉及原始碼,也沒有涉及太多的原理,如果讀者希望瞭解更多關於三者的內容,建議參看一些書籍,例如老外寫的《Java NIO》,該書全面系統的講解了NIO的各種元件和細節,非常推薦。