導(dǎo)讀:本文你將獲取到:同/異步 + 阻/非阻塞的性能區(qū)別术裸;BIO亭枷、NIO叨粘、AIO 的區(qū)別瘤睹;理解和實(shí)現(xiàn) NIO 操作 Socket 時(shí)的多路復(fù)用轰传;同時(shí)掌握 IO 最底層最核心的操作技巧获茬。
BIO倔既、NIO渤涌、AIO 的區(qū)別是什么把还?
同/異步吊履、阻/非阻塞的區(qū)別是什么?
文件讀寫(xiě)最優(yōu)雅的實(shí)現(xiàn)方式是什么酌伊?
NIO 如何實(shí)現(xiàn)多路復(fù)用功能腺晾?
帶著以上這幾個(gè)問(wèn)題辜贵,讓我們一起進(jìn)入IO的世界吧。
在開(kāi)始之前鼻由,我們先來(lái)思考一個(gè)問(wèn)題:我們經(jīng)常所說(shuō)的“IO”的全稱(chēng)到底是什么厚棵?
可能很多人看到這個(gè)問(wèn)題和我一樣一臉懵逼婆硬,IO的全稱(chēng)其實(shí)是:Input/Output的縮寫(xiě)。
一向楼、IO 介紹
我們通常所說(shuō)的 BIO 是相對(duì)于 NIO 來(lái)說(shuō)的谐区,BIO 也就是 Java 開(kāi)始之初推出的 IO 操作模塊,BIO 是 BlockingIO 的縮寫(xiě)昭抒,顧名思義就是阻塞 IO 的意思灭返。
1.1 BIO、NIO诈乒、AIO的區(qū)別
BIO 就是傳統(tǒng)的 java.io 包怕磨,它是基于流模型實(shí)現(xiàn)的,交互的方式是同步消约、阻塞方式肠鲫,也就是說(shuō)在讀入輸入流或者輸出流時(shí),在讀寫(xiě)動(dòng)作完成之前或粮,線程會(huì)一直阻塞在那里导饲,它們之間的調(diào)用時(shí)可靠的線性順序。它的有點(diǎn)就是代碼比較簡(jiǎn)單氯材、直觀渣锦;缺點(diǎn)就是 IO 的效率和擴(kuò)展性很低,容易成為應(yīng)用性能瓶頸氢哮。
NIO 是 Java 1.4 引入的 java.nio 包,提供了 Channel听盖、Selector、Buffer 等新的抽象背零,可以構(gòu)建多路復(fù)用的蝎困、同步非阻塞 IO 程序,同時(shí)提供了更接近操作系統(tǒng)底層高性能的數(shù)據(jù)操作方式虽缕。
AIO 是 Java 1.7 之后引入的包伍派,是 NIO 的升級(jí)版本诉植,提供了異步非堵塞的 IO 操作方式,所以人們叫它 AIO(Asynchronous IO)灼擂,異步 IO 是基于事件和回調(diào)機(jī)制實(shí)現(xiàn)的剔应,也就是應(yīng)用操作之后會(huì)直接返回,不會(huì)堵塞在那里纤控,當(dāng)后臺(tái)處理完成嚼黔,操作系統(tǒng)會(huì)通知相應(yīng)的線程進(jìn)行后續(xù)的操作。
1.2 全面認(rèn)識(shí) IO
傳統(tǒng)的 IO 大致可以分為4種類(lèi)型:
InputStream碎节、OutputStream 基于字節(jié)操作的 IO
Writer狮荔、Reader 基于字符操作的 IO
File 基于磁盤(pán)操作的 IO
Socket 基于網(wǎng)絡(luò)操作的 IO
java.net 下提供的 Scoket 很多時(shí)候人們也把它歸為 同步阻塞 IO ,因?yàn)榫W(wǎng)絡(luò)通訊同樣是 IO 行為。
java.io 下的類(lèi)和接口很多雅采,但大體都是 InputStream宝鼓、OutputStream愚铡、Writer、Reader 的子集营曼,所有掌握這4個(gè)類(lèi)和File的使用蒂阱,是用好 IO 的關(guān)鍵。
1.3 IO 使用
接下來(lái)看 InputStream妈踊、OutputStream廊营、Writer、Reader 的繼承關(guān)系圖和使用示例慎式。
1.3.1 InputStream 使用
繼承關(guān)系圖和類(lèi)方法,如下圖:
InputStream 使用示例:
InputStream inputStream =newFileInputStream("D:\\log.txt");byte[] bytes =newbyte[inputStream.available()];inputStream.read(bytes);String str =newString(bytes,"utf-8");System.out.println(str);inputStream.close();
1.3.2 OutputStream 使用
繼承關(guān)系圖和類(lèi)方法,如下圖:
OutputStream 使用示例:
OutputStream outputStream =newFileOutputStream("D:\\log.txt",true);// 參數(shù)二扇救,表示是否追加,true=追加outputStream.write("你好沧烈,老王".getBytes("utf-8"));outputStream.close();
1.3.3 Writer 使用
Writer 繼承關(guān)系圖和類(lèi)方法,如下圖:
Writer 使用示例:
Writer writer =newFileWriter("D:\\log.txt",true);// 參數(shù)二,是否追加文件侈贷,true=追加writer.append("老王撑蚌,你好");writer.close();
1.3.4 Reader 使用
Reader 繼承關(guān)系圖和類(lèi)方法,如下圖:
Reader 使用示例:
Reader reader =newFileReader(filePath);BufferedReader bufferedReader =newBufferedReader(reader);StringBufferbf =newStringBuffer();Stringstr;while((str = bufferedReader.readLine()) !=null) {? ? bf.append(str +"\n");}bufferedReader.close();reader.close();System.out.println(bf.toString());
二亮垫、同步、異步害晦、阻塞壹瘟、非阻塞
上面說(shuō)了很多關(guān)于同步、異步雕凹、阻塞和非阻塞的概念线欲,接下來(lái)就具體聊一下它們4個(gè)的含義苦锨,以及組合之后形成的性能分析。
2.1 同步與異步
同步就是一個(gè)任務(wù)的完成需要依賴(lài)另外一個(gè)任務(wù)時(shí)秃励,只有等待被依賴(lài)的任務(wù)完成后,依賴(lài)的任務(wù)才能算完成谣旁,這是一種可靠的任務(wù)序列。要么成功都成功杆麸,失敗都失敗饼问,兩個(gè)任務(wù)的狀態(tài)可以保持一致。而異步是不需要等待被依賴(lài)的任務(wù)完成盅视,只是通知被依賴(lài)的任務(wù)要完成什么工作,依賴(lài)的任務(wù)也立即執(zhí)行贺归,只要自己完成了整個(gè)任務(wù)就算完成了。至于被依賴(lài)的任務(wù)最終是否真正完成踱葛,依賴(lài)它的任務(wù)無(wú)法確定光坝,所以它是不可靠的任務(wù)序列洲赵。我們可以用打電話和發(fā)短信來(lái)很好的比喻同步與異步操作芝发。
2.2 阻塞與非阻塞
阻塞與非阻塞主要是從 CPU 的消耗上來(lái)說(shuō)的腹殿,阻塞就是 CPU 停下來(lái)等待一個(gè)慢的操作完成 CPU 才接著完成其它的事刻炒。非阻塞就是在這個(gè)慢的操作在執(zhí)行時(shí) CPU 去干其它別的事拇厢,等這個(gè)慢的操作完成時(shí)管行,CPU 再接著完成后續(xù)的操作。雖然表面上看非阻塞的方式可以明顯的提高 CPU 的利用率废赞,但是也帶了另外一種后果就是系統(tǒng)的線程切換增加。增加的 CPU 使用時(shí)間能不能補(bǔ)償系統(tǒng)的切換成本需要好好評(píng)估耘沼。
2.3 同/異、阻/非堵塞 組合
同/異、阻/非堵塞的組合者春,有四種類(lèi)型校仑,如下表:
組合方式性能分析
同步阻塞最常用的一種用法稻扬,使用也是最簡(jiǎn)單的,但是 I/O 性能一般很差,CPU 大部分在空閑狀態(tài)黔宛。
同步非阻塞提升 I/O 性能的常用手段,就是將 I/O 的阻塞改成非阻塞方式案淋,尤其在網(wǎng)絡(luò) I/O 是長(zhǎng)連接,同時(shí)傳輸數(shù)據(jù)也不是很多的情況下,提升性能非常有效旨涝。 這種方式通常能提升 I/O 性能贩耐,但是會(huì)增加CPU 消耗,要考慮增加的 I/O 性能能不能補(bǔ)償 CPU 的消耗,也就是系統(tǒng)的瓶頸是在 I/O 還是在 CPU 上奇钞。
異步阻塞這種方式在分布式數(shù)據(jù)庫(kù)中經(jīng)常用到,例如在網(wǎng)一個(gè)分布式數(shù)據(jù)庫(kù)中寫(xiě)一條記錄,通常會(huì)有一份是同步阻塞的記錄,而還有兩至三份是備份記錄會(huì)寫(xiě)到其它機(jī)器上册着,這些備份記錄通常都是采用異步阻塞的方式寫(xiě) I/O。異步阻塞對(duì)網(wǎng)絡(luò) I/O 能夠提升效率,尤其像上面這種同時(shí)寫(xiě)多份相同數(shù)據(jù)的情況。
異步非阻塞這種組合方式用起來(lái)比較復(fù)雜,只有在一些非常復(fù)雜的分布式情況下使用,像集群之間的消息同步機(jī)制一般用這種 I/O 組合方式伞租。如 Cassandra 的 Gossip 通信機(jī)制就是采用異步非阻塞的方式作喘。它適合同時(shí)要傳多份相同的數(shù)據(jù)到集群中不同的機(jī)器广辰,同時(shí)數(shù)據(jù)的傳輸量雖然不大,但是卻非常頻繁。這種網(wǎng)絡(luò) I/O 用這個(gè)方式性能能達(dá)到最高夯接。
三逊拍、優(yōu)雅的文件讀寫(xiě)
Java 7 之前文件的讀取是這樣的:
// 添加文件FileWriter fileWriter =newFileWriter(filePath,true);fileWriter.write(Content);fileWriter.close();// 讀取文件FileReader fileReader =newFileReader(filePath);BufferedReader bufferedReader =newBufferedReader(fileReader);StringBuffer bf =newStringBuffer();String str;while ((str = bufferedReader.readLine()) != null) {? ? bf.append(str +"\n");}bufferedReader.close();fileReader.close();System.out.println(bf.toString());
Java 7 引入了Files(java.nio包下)的世曾,大大簡(jiǎn)化了文件的讀寫(xiě),如下:
// 寫(xiě)入文件(追加方式:StandardOpenOption.APPEND)Files.write(Paths.get(filePath), Content.getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND);// 讀取文件byte[]data= Files.readAllBytes(Paths.get(filePath));System.out.println(new String(data, StandardCharsets.UTF_8));
讀寫(xiě)文件都是一行代碼搞定,沒(méi)錯(cuò)這就是最優(yōu)雅的文件操作玫恳。
Files 下還有很多有用的方法,比如創(chuàng)建多層文件夾,寫(xiě)法上也簡(jiǎn)單了:
// 創(chuàng)建多(單)層目錄(如果不存在創(chuàng)建换吧,存在不會(huì)報(bào)錯(cuò))newFile("D://a//b").mkdirs();
四贯莺、Socket 和 NIO 的多路復(fù)用
本節(jié)帶你實(shí)現(xiàn)最基礎(chǔ)的 Socket 的同時(shí)爹耗,同時(shí)會(huì)實(shí)現(xiàn) NIO 多路復(fù)用,還有 AIO 中 Socket 的實(shí)現(xiàn)。
4.1 傳統(tǒng)的 Socket 實(shí)現(xiàn)
接下來(lái)我們將會(huì)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 Socket炫狱,服務(wù)器端只發(fā)給客戶端信息,再由客戶端打印出來(lái)的例子汪茧,代碼如下:
intport =4343;//端口號(hào)// Socket 服務(wù)器端(簡(jiǎn)單的發(fā)送信息)Thread sThread =newThread(newRunnable() {@Overridepublicvoidrun(){try{? ? ? ? ? ? ServerSocket serverSocket =newServerSocket(port);while(true) {// 等待連接Socket socket = serverSocket.accept();? ? ? ? ? ? ? ? Thread sHandlerThread =newThread(newRunnable() {@Overridepublicvoidrun(){try(PrintWriter printWriter =newPrintWriter(socket.getOutputStream())) {? ? ? ? ? ? ? ? ? ? ? ? ? ? printWriter.println("hello world!");? ? ? ? ? ? ? ? ? ? ? ? ? ? printWriter.flush();? ? ? ? ? ? ? ? ? ? ? ? }catch(IOException e) {? ? ? ? ? ? ? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? });? ? ? ? ? ? ? ? sHandlerThread.start();? ? ? ? ? ? }? ? ? ? }catch(IOException e) {? ? ? ? ? ? e.printStackTrace();? ? ? ? }? ? }});sThread.start();// Socket 客戶端(接收信息并打又椴濉)try(Socket cSocket =newSocket(InetAddress.getLocalHost(), port)) {? ? BufferedReader bufferedReader =newBufferedReader(newInputStreamReader(cSocket.getInputStream()));? ? bufferedReader.lines().forEach(s -> System.out.println("客戶端:"+ s));}catch(UnknownHostException e) {? ? e.printStackTrace();}catch(IOException e) {? ? e.printStackTrace();}
調(diào)用 accept 方法训堆,阻塞等待客戶端連接鲁沥;
利用 Socket 模擬了一個(gè)簡(jiǎn)單的客戶端,只進(jìn)行連接允扇、讀取和打雍巍;
在 Java 中赃蛛,線程的實(shí)現(xiàn)是比較重量級(jí)的,所以線程的啟動(dòng)或者銷(xiāo)毀是很消耗服務(wù)器的資源的,即使使用線程池來(lái)實(shí)現(xiàn)实束,使用上述傳統(tǒng)的 Socket 方式悼瘾,當(dāng)連接數(shù)極具上升也會(huì)帶來(lái)性能瓶頸,原因是線程的上線文切換開(kāi)銷(xiāo)會(huì)在高并發(fā)的時(shí)候體現(xiàn)的很明顯,并且以上操作方式還是同步阻塞式的編程,性能問(wèn)題在高并發(fā)的時(shí)候就會(huì)體現(xiàn)的尤為明顯富稻。
以上的流程向抢,如下圖:
4.2 NIO 多路復(fù)用
介于以上高并發(fā)的問(wèn)題,NIO 的多路復(fù)用功能就顯得意義非凡了。
NIO 是利用了單線程輪詢事件的機(jī)制,通過(guò)高效地定位就緒的 Channel徙鱼,來(lái)決定做什么距淫,僅僅 select 階段是阻塞的蓬衡,可以有效避免大量客戶端連接時(shí)盟猖,頻繁線程切換帶來(lái)的問(wèn)題惊豺,應(yīng)用的擴(kuò)展能力有了非常大的提高萍程。
// NIO 多路復(fù)用ThreadPoolExecutor threadPool =newThreadPoolExecutor(4,4,60L, TimeUnit.SECONDS,newLinkedBlockingQueue());threadPool.execute(newRunnable() {? ? @Overridepublicvoidrun(){try(Selector selector = Selector.open();? ? ? ? ? ? ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();) {? ? ? ? ? ? serverSocketChannel.bind(newInetSocketAddress(InetAddress.getLocalHost(), port));? ? ? ? ? ? serverSocketChannel.configureBlocking(false);? ? ? ? ? ? serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);while(true) {? ? ? ? ? ? ? ? selector.select();// 阻塞等待就緒的ChannelSet selectionKeys = selector.selectedKeys();? ? ? ? ? ? ? ? Iterator iterator = selectionKeys.iterator();while(iterator.hasNext()) {? ? ? ? ? ? ? ? ? ? SelectionKey key = iterator.next();try(SocketChannel channel = ((ServerSocketChannel) key.channel()).accept()) {? ? ? ? ? ? ? ? ? ? ? ? channel.write(Charset.defaultCharset().encode("你好朽褪,世界"));? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? iterator.remove();? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }catch(IOException e) {? ? ? ? ? ? e.printStackTrace();? ? ? ? }? ? }});// Socket 客戶端(接收信息并打印)try(Socket cSocket =newSocket(InetAddress.getLocalHost(), port)) {? ? BufferedReader bufferedReader =newBufferedReader(newInputStreamReader(cSocket.getInputStream()));? ? bufferedReader.lines().forEach(s -> System.out.println("NIO 客戶端:"+ s));}catch(IOException e) {? ? e.printStackTrace();}
首先,通過(guò) Selector.open() 創(chuàng)建一個(gè) Selector输莺,作為類(lèi)似調(diào)度員的角色疏唾;
然后祖灰,創(chuàng)建一個(gè) ServerSocketChannel,并且向 Selector 注冊(cè)畔规,通過(guò)指定 SelectionKey.OP_ACCEPT局扶,告訴調(diào)度員,它關(guān)注的是新的連接請(qǐng)求叁扫;
為什么我們要明確配置非阻塞模式呢三妈?這是因?yàn)樽枞J较拢?cè)操作是不允許的莫绣,會(huì)拋出 IllegalBlockingModeException 異常畴蒲;
Selector 阻塞在 select 操作,當(dāng)有 Channel 發(fā)生接入請(qǐng)求对室,就會(huì)被喚醒模燥;
下面的圖,可以有效的說(shuō)明 NIO 復(fù)用的流程:
就這樣 NIO 的多路復(fù)用就大大提升了服務(wù)器端響應(yīng)高并發(fā)的能力掩宜。
4.3 AIO 版 Socket 實(shí)現(xiàn)
Java 1.7 提供了 AIO 實(shí)現(xiàn)的 Socket 是這樣的蔫骂,如下代碼:
// AIO線程復(fù)用版Thread sThread =newThread(newRunnable() {? ? @Overridepublicvoidrun() {? ? ? ? AsynchronousChannelGroupgroup=null;try{group= AsynchronousChannelGroup.withThreadPool(Executors.newFixedThreadPool(4));? ? ? ? ? ? AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(group).bind(newInetSocketAddress(InetAddress.getLocalHost(), port));? ? ? ? ? ? server.accept(null,newCompletionHandler() {? ? ? ? ? ? ? ? @Overridepublicvoidcompleted(AsynchronousSocketChannel result, AsynchronousServerSocketChannel attachment) {? ? ? ? ? ? ? ? ? ? server.accept(null,this);// 接收下一個(gè)請(qǐng)求try{? ? ? ? ? ? ? ? ? ? ? ? Future f = result.write(Charset.defaultCharset().encode("你好,世界"));? ? ? ? ? ? ? ? ? ? ? ? f.get();? ? ? ? ? ? ? ? ? ? ? ? System.out.println("服務(wù)端發(fā)送時(shí)間:"+newSimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(newDate()));? ? ? ? ? ? ? ? ? ? ? ? result.close();? ? ? ? ? ? ? ? ? ? }catch(InterruptedException | ExecutionException | IOException e) {? ? ? ? ? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? @Overridepublicvoidfailed(Throwable exc, AsynchronousServerSocketChannel attachment) {? ? ? ? ? ? ? ? }? ? ? ? ? ? });group.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);? ? ? ? }catch(IOException | InterruptedException e) {? ? ? ? ? ? e.printStackTrace();? ? ? ? }? ? }});sThread.start();// Socket 客戶端AsynchronousSocketChannel client = AsynchronousSocketChannel.open();Future future = client.connect(newInetSocketAddress(InetAddress.getLocalHost(), port));future.get();ByteBuffer buffer = ByteBuffer.allocate(100);client.read(buffer,null,newCompletionHandler() {? ? @Overridepublicvoidcompleted(Integer result,Voidattachment) {? ? ? ? System.out.println("客戶端打游馈:"+newString(buffer.array()));? ? }? ? @Overridepublicvoidfailed(Throwable exc,Voidattachment) {? ? ? ? exc.printStackTrace();try{? ? ? ? ? ? client.close();? ? ? ? }catch(IOException e) {? ? ? ? ? ? e.printStackTrace();? ? ? ? }? ? }});Thread.sleep(10*1000);
五辽旋、總結(jié)
以上基本就是 IO 從 1.0 到目前版本(本文的版本)JDK 8 的核心使用操作了,可以看出來(lái) IO 作為比較常用的基礎(chǔ)功能,發(fā)展變化的改動(dòng)也很大补胚,而且使用起來(lái)也越來(lái)越簡(jiǎn)單了固该,IO 的操作也是比較好理解的,一個(gè)輸入一個(gè)輸出糖儡,掌握好了輸入輸出也就掌握好了 IO,Socket 作為網(wǎng)絡(luò)交互的集成功能怔匣,顯然 NIO 的多路復(fù)用握联,給 Socket 帶來(lái)了更多的活力和選擇,用戶可以根據(jù)自己的實(shí)際場(chǎng)景選擇相應(yīng)的代碼策略每瞒。