到現(xiàn)在為止,Java IO可分為三類:BIO火架、NIO鉴象、AIO。最早出現(xiàn)的是BIO距潘,然后是NIO炼列,最近的是AIO,BIO即Blocking IO音比,NIO有的文章說是New NIO俭尖,也有的文章說是No Blocking IO,我查了一些資料洞翩,官網(wǎng)說的應(yīng)該是No Blocking IO稽犁,提供了Selector,Channle骚亿,SelectionKey抽象已亥,AIO即Asynchronous IO(異步IO),提供了Fauture等異步操作来屠。
1 BIO
上圖是BIO的架構(gòu)體系圖虑椎。可以看到BIO主要分為兩類IO俱笛,即字符流IO和字節(jié)流IO捆姜,字符流即把輸入輸出數(shù)據(jù)當(dāng)做字符來看待,Writer和Reader是其繼承體系的最高層迎膜,字節(jié)流即把輸入輸出當(dāng)做字節(jié)來看待泥技,InputStream和OutputStream是其繼承體系的最高層。下面以文件操作為例磕仅,其他的實現(xiàn)類也非常類似珊豹。
順便說一下簸呈,整個BIO體系大量使用了裝飾者模式,例如BufferedInputStream包裝了InputStream店茶,使其擁有了緩沖的能力蜕便。
1.1 字節(jié)流
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構(gòu)造函數(shù)中傳入文件名來創(chuàng)建FileOutputStream對象,即打開了一個字節(jié)流忽妒,之后使用write方法向字節(jié)流中寫入數(shù)據(jù)玩裙,完成之后調(diào)用flush刷新緩沖區(qū),最后記得要關(guān)閉字節(jié)流段直。讀取文件也是類似的吃溅,先打開一個字節(jié)流,然后從字節(jié)流中讀取數(shù)據(jù)并存入內(nèi)存中(buffer數(shù)組)鸯檬,然后再關(guān)閉字節(jié)流决侈。
因為InputStream和OutputStream都繼承了AutoCloseable接口,所以如果使用的是try-resource的語法來進(jìn)行字節(jié)流的IO操作喧务,可不需要手動顯式調(diào)用close方法了赖歌,這也是非常推薦的做法,在示例中我沒有這樣做只是為了方便功茴。
1.2 字符流
字節(jié)流主要使用的是InputStream和OutputStream庐冯,而字符流主要使用的就是與之對應(yīng)的Reader和Writer。下面來看一個示例坎穿,該示例的功能和上述示例的一樣展父,只不過實現(xiàn)手段不同:
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非常簡單,無法就是打開字符流玲昧,然后向字符流中寫入字符栖茉,然后關(guān)閉。關(guān)鍵是Reader孵延,示例代碼中使用了BufferedReader來包裝FileReader吕漂,使得原本沒有緩沖功能的FileReader有了緩沖功能,這就是上面提到過的裝飾者模式尘应,BufferedReader還提供了方便使用的API惶凝,例如readLine(),這個方法每次調(diào)用會讀取文件中的一行犬钢。
以上就是BIO的簡單使用梨睁,源碼的話因為涉及太多的底層,所以如果對底層不是很了解的話會很難理解源碼娜饵。
2 NIO
BIO是同步阻塞的IO,而NIO是同步非阻塞的IO官辈。NIO中有幾個比較重要的組件:Selector箱舞,SelectionKey遍坟,Channel,ByteBuffer晴股,其中Selector就是所謂的選擇器愿伴,SelectionKey可以簡單理解為選擇鍵,這個鍵將Selector和Channle進(jìn)行一個綁定(或者所Channle注冊到Selector上)电湘,當(dāng)有數(shù)據(jù)到達(dá)Channel的時候隔节,Selector會從阻塞狀態(tài)中恢復(fù)過來,并對該Channle進(jìn)行操作寂呛,并且怎诫,我們不能直接對Channle進(jìn)行讀寫操作,只能對ByteBuffer操作贷痪。如下圖所示:
下面是一個Socket網(wǎng)絡(luò)編程的例子:
//服務(wù)端
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();
//獲取一個服務(wù)端socket Channel
ServerSocketChannel channel = ServerSocketChannel.open();
//設(shè)置為非阻塞模式
channel.configureBlocking(false);
//綁定端口
channel.socket().bind(new InetSocketAddress(port));
//把channle注冊到Selector上幻妓,并表示對ACCEPT事件感興趣
SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
//該方法會阻塞,直到和其綁定的任何一個channel有數(shù)據(jù)過來
selector.select();
//獲取該Selector綁定的SelectionKey
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
//記得刪除劫拢,否則就無限循環(huán)了
iterator.remove();
//如果該事件是一個ACCEPT肉津,那么就執(zhí)行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進(jìn)行讀寫操作,只能對ByteBuffer進(jìn)行操作
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();
}
}
//當(dāng)有連接過來的時候熟吏,獲取連接過來的channle距糖,然后注冊到Selector上,并設(shè)置成對讀消息感興趣分俯,當(dāng)客戶端有消息過來的時候肾筐,Selector就可以讓其執(zhí)行doRead方法,然后讀取消息并打印缸剪。
private void doAccept(SelectionKey key) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
System.out.println("服務(wù)端監(jiān)聽中...");
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();
//連接到遠(yuǎn)程服務(wù)器
channel.connect(new InetSocketAddress(port));
//設(shè)置非阻塞模式
channel.configureBlocking(false);
//往ByteBuffer里寫消息
ByteBuffer buffer = ByteBuffer.allocate(BUF);
buffer.put("Hello,Server".getBytes("UTF-8"));
buffer.flip();
//將ByteBuffer內(nèi)容寫入Channle,即發(fā)送消息
channel.write(buffer);
channel.close();
}
public static void main(String[] args) throws IOException {
SocketClient client = new SocketClient();
client.init();
}
}
嘗試啟動一個服務(wù)端杏节,多個客戶端唬渗,結(jié)果大致如下所示:
服務(wù)端監(jiān)聽中...
讀取到消息: Hello,Server
服務(wù)端監(jiān)聽中...
讀取到消息: Hello,Server
注釋寫得挺清楚了,我這里只是簡單使用了NIO奋渔,但實際上NIO遠(yuǎn)遠(yuǎn)不止這些東西镊逝,光一個ByteBuffer就能說一天,如果有機(jī)會嫉鲸,我會在后面Netty相關(guān)的文章中詳細(xì)說一下這幾個組件撑蒜。在此就不再多說了。
吐槽一些,純NIO寫的服務(wù)端和客戶端實在是太麻煩了座菠,一不小心就會寫錯狸眼,還是使用Netty類似的框架好一些啊。
3 AIO
在JDK7中新增了一些IO相關(guān)的API浴滴,這些API稱作AIO拓萌。因為其提供了一些異步操作IO的功能,但本質(zhì)是其實還是NIO升略,所以可以簡單的理解為是NIO的擴(kuò)充微王。AIO中最重要的就是Future了,F(xiàn)uture表示將來的意思品嚣,即這個操作可能會持續(xù)很長時間炕倘,但我不會等,而是到將來操作完成的時候腰根,再過來通知我激才,這就是異步的意思。下面是兩個使用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(); //阻塞额嘿,如果不調(diào)用該方法瘸恼,main方法會繼續(xù)執(zhí)行
buffer.flip();
System.out.println(new String(buffer.array(), "UTF-8"));
System.out.println(readNum);
}
第一個例子使用AsynchronousFileChannel來異步的讀取文件內(nèi)容,在代碼中册养,我使用了future.get()方法东帅,該方法會阻塞當(dāng)前線程,在例子中即主線程球拦,當(dāng)工作線程靠闭,即讀取文件的線程執(zhí)行完畢后才會從阻塞狀態(tài)中恢復(fù)過來,并將結(jié)果返回坎炼。之后就可以從ByteBuffer中讀取數(shù)據(jù)了愧膀。這是使用將來時的例子,下面來看看使用回調(diào)的例子:
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("繼續(xù)執(zhí)行主線程");
//調(diào)用完成之后不需要等待任務(wù)完成谣光,會直接繼續(xù)執(zhí)行主線程
while (true) {
Thread.sleep(1000);
}
}
}
輸出的結(jié)果大致如下所示檩淋,但不一定,這取決于線程調(diào)度:
繼續(xù)執(zhí)行主線程
完成讀取
hello,world
hello,yeonon
當(dāng)任務(wù)完成萄金,即讀取文件完畢的時候蟀悦,會調(diào)用completed方法,失敗會調(diào)用failed方法氧敢,這就是回調(diào)日戈。詳細(xì)接觸過回調(diào)的朋友應(yīng)該不難理解。
4 BIO孙乖、NIO浙炼、AIO的區(qū)別
- BIO是同步阻塞的IO份氧,NIO是同步非阻塞IO,AIO異步非阻塞IO鼓拧,這是最基本的區(qū)別半火。阻塞模式會導(dǎo)致其他線程被IO線程阻塞,必須等待IO線程執(zhí)行完畢才能繼續(xù)執(zhí)行邏輯季俩,非阻塞和異步并不等同,非阻塞模式下梅掠,一般會采用事件輪詢的方式來執(zhí)行IO酌住,即IO多路復(fù)用,雖然仍然是同步的阎抒,但執(zhí)行效率比傳統(tǒng)的BIO要高很多酪我,AIO則是異步IO,如果把IO工作當(dāng)做一個任務(wù)的話且叁,在當(dāng)前線程中提交一個任務(wù)之后都哭,不會有阻塞,會繼續(xù)執(zhí)行當(dāng)前線程的后續(xù)邏輯逞带,在任務(wù)完成之后欺矫,當(dāng)前線程會收到通知,然后再決定如何處理展氓,這種方式的IO穆趴,CPU效率是最高的,CPU幾乎沒有發(fā)生過停頓遇汞,而時一直至于忙狀態(tài)未妹,所以效率非常高,但編程難度會比較大空入。
- BIO面向的是流络它,無論是字符流還是字節(jié)流,通俗的講歪赢,BIO在讀寫數(shù)據(jù)的時候會按照一個接一個的方式讀寫化戳,而NIO和AIO(因為AIO實際上是NIO的擴(kuò)充,所以從這個方面來看轨淌,可以把他們放在一塊)讀寫數(shù)據(jù)的時候是按照一塊一塊的讀取的迂烁,讀取到的數(shù)據(jù)會緩存在內(nèi)存中,然后在內(nèi)存中對數(shù)據(jù)進(jìn)行處理递鹉,這種方式的好處是減少了硬盤或者網(wǎng)絡(luò)的讀寫次數(shù)盟步,從而降低了由于硬盤或網(wǎng)絡(luò)速度慢帶來的效率影響。
- BIO的API雖然比較底層躏结,但如果熟悉之后編寫起來會比較容易却盘,NIO或者AIO的API抽象層次高,一般來說應(yīng)該更容易使用才是,但實際上卻很難“正確”的編寫黄橘,而且DEBUG的難度也較大兆览,這也是為什么Netty等NIO框架受歡迎的原因之一。
以上就是我理解的BIO塞关、NIO和AIO區(qū)別抬探。
5 小結(jié)
本文簡單粗略的講了一下BIO、NIO帆赢、AIO的使用小压,并未涉及源碼,也沒有涉及太多的原理椰于,如果讀者希望了解更多關(guān)于三者的內(nèi)容怠益,建議參看一些書籍,例如老外寫的《Java NIO》瘾婿,該書全面系統(tǒng)的講解了NIO的各種組件和細(xì)節(jié)蜻牢,非常推薦。