探秘JDK 7之四:下一代I/O(NIO.2)
51CTO曾在Java 7 下一代Java開(kāi)發(fā)技術(shù)詳解專題里對(duì)“JDK 7 I/O新功能”有過(guò)簡(jiǎn)單地介紹,其實(shí)早在2000年的時(shí)候,Sun公司就啟動(dòng)了JSR 51:為Java平臺(tái)開(kāi)發(fā)新的I/O API,直接訪問(wèn)操作系統(tǒng)底層輸入/輸出操作以提高應(yīng)用程序的性能,首次引入這套API是在J2SE 1.4中,根據(jù)維基百科的新I/O詞條顯示,新I/O(NIO)由下列API組成:
◆ 原始類型數(shù)據(jù)緩沖
◆ 字符集編碼和解碼
◆ 通道,新的原始I/O抽象
◆ 支持上鎖和內(nèi)存映射的文件接口,文件最大支持Integer.MAX_VALUE字節(jié)(2GB)
◆ 為可擴(kuò)展服務(wù)器提供的多路復(fù)用,無(wú)阻塞I/O設(shè)施(基于選擇器和鍵)
JSR 203(NIO.2)除了解決JSR 51遺留下來(lái)的問(wèn)題外,還為Java平臺(tái)提供了更多新的I/O API,NIO.2解決了java.awt.File文件系統(tǒng)接口存在的重大問(wèn)題,引入了異步I/O,并完成了未包括在JSR 51中的功能,下面列出了包含在JSR 203中的主要組件:
◆ 新的文件系統(tǒng)接口,支持大塊訪問(wèn)文件屬性,更改通知,繞開(kāi)文件系統(tǒng)指定的API,也是可插拔文件系統(tǒng)實(shí)現(xiàn)的服務(wù)提供者接口。
◆ 對(duì)套接字和文件同時(shí)提供了異步I/O操作的API。
◆ JSR 51中定義的完整的套接字通道功能,此外還包括綁定,選項(xiàng)配置和多播數(shù)據(jù)報(bào)的支持。
新的文件系統(tǒng)接口
Java的File類存在重大問(wèn)題,例如,操作出錯(cuò)時(shí),delete()和mkdir()方法返回一個(gè)狀態(tài)碼而不是一個(gè)異常,沒(méi)有辦法獲知失敗的原因,此外還包括以下問(wèn)題:
◆ File沒(méi)有提供方法來(lái)檢測(cè)符號(hào)鏈接,要知道為什么檢測(cè)符號(hào)鏈接很重要,以及如何解決這個(gè)問(wèn)題的辦法,請(qǐng)參考Patrick的文章“在Java中如何處理文件系統(tǒng)軟鏈接/符號(hào)鏈接”和“Java中的鏈接/別名/快捷方式”。
◆ File提供的方法只能訪問(wèn)部分文件屬性,不能訪問(wèn)文件權(quán)限和訪問(wèn)控制列表。
◆ File沒(méi)有提供方法一次訪問(wèn)文件的所有屬性(如文件的修改時(shí)間和它的類型),因?yàn)槲募到y(tǒng)需要為每個(gè)屬性執(zhí)行查詢請(qǐng)求,可能存在性能問(wèn)題。
◆ File的list()和listFiles()方法返回文件名和目錄名的數(shù)組,但不支持大目錄,通過(guò)網(wǎng)絡(luò)展示大目錄清單時(shí),調(diào)用list()/listFiles()方法可能會(huì)使當(dāng)前的線程阻塞相當(dāng)長(zhǎng)一段時(shí)間,而在服務(wù)器端,虛擬機(jī)可能會(huì)耗盡內(nèi)存。
◆ File沒(méi)有提供復(fù)制和移動(dòng)文件的方法,雖然File提供了一個(gè)renameTo()方法在某些時(shí)候可以用來(lái)移動(dòng)文件,但它的行為與平臺(tái)關(guān)系緊密,即在不同平臺(tái)上的行為是不一致的,根據(jù)renameTo()的文檔說(shuō)明,這個(gè)方法不能在文件系統(tǒng)之間移動(dòng)文件,它可能不是原子的,如果目標(biāo)路徑下已存在同名文件,這個(gè)操作可能不會(huì)成功。
◆ File也沒(méi)有提供改變通知方法,需要應(yīng)用程序自己實(shí)現(xiàn),因此導(dǎo)致應(yīng)用程序的性能下降,例如,服務(wù)器需要確定什么時(shí)候往目錄中添加了一個(gè)新的JAR文件,它需要實(shí)時(shí)監(jiān)視這個(gè)目錄,因?yàn)榉?wù)器后臺(tái)線程需要頻繁讀取文件系統(tǒng),因此性能會(huì)有所下降。
◆ File也不允許開(kāi)發(fā)人員引入他們自己的文件系統(tǒng)訪問(wèn)功能,例如,開(kāi)發(fā)人員可能想將文件系統(tǒng)存儲(chǔ)到一個(gè)zip文件中,或創(chuàng)建一個(gè)內(nèi)存文件系統(tǒng)。
NIO.2引入了新的文件系統(tǒng)接口,除了解決上述存在的問(wèn)題外,還引入了更多的功能,這個(gè)接口由位于java.nio.file,java.nio.file.attribute和java.nio.file.spi包中的類和其它類型組成。
這些包提供了多個(gè)切入點(diǎn),其中一個(gè)切入點(diǎn)就是java.nio.file.Paths類,它提供了兩個(gè)方法返回一個(gè)java.nio.file.Path實(shí)例:
◆ public static Path get(String path) – 它通過(guò)轉(zhuǎn)換給定路徑字符串返回給這個(gè)實(shí)例構(gòu)造一個(gè)Path實(shí)例。
◆ public static Path get(URI uri) -它通過(guò)轉(zhuǎn)換給定路徑的URI(統(tǒng)一資源定位符)返回給這個(gè)實(shí)例構(gòu)造一個(gè)Path實(shí)例。
與傳統(tǒng)的基于File的代碼互操作:
File類提供了一個(gè)public Path toPath()方法,它可以將一個(gè)File實(shí)例轉(zhuǎn)換成一個(gè)Path實(shí)例。
當(dāng)你創(chuàng)建了一個(gè)Path實(shí)例后,你就可以使用這個(gè)實(shí)例執(zhí)行許多路徑操作(如返回路徑的一部分,連接兩個(gè)路徑)和許多文件操作(如刪除,移動(dòng)和復(fù)制文件)。
為了不將問(wèn)題復(fù)雜化,我就不深入講解Path了,這里我用一段代碼簡(jiǎn)單地演示一下以前的get()方法和Path的delete()方法。
清單1. InformedDelete.java
- // InformedDelete.java
- import java.io.IOException;
- import java.nio.file.DirectoryNotEmptyException;
- import java.nio.file.NoSuchFileException;
- import java.nio.file.Path;
- import java.nio.file.Paths;
- public class InformedDelete
- {
- public static void main (String [] args)
- {
- if (args.length != 1)
- {
- System.err.println ("usage: java InformedDelete path");
- return;
- }
- // Attempt to construct a Path instance by converting the path argument
- // string. If unsuccessful (you passed an empty string as the
- // command-line argument), the get() method throws an instance of the
- // unchecked java.nio.file.InvalidPathException class.
- Path path = Paths.get (args [0]);
- try
- {
- path.delete (); // Attempt to delete the path.
- }
- catch (NoSuchFileException e)
- {
- System.err.format ("%s: no such file or directory%n", path);
- }
- catch (DirectoryNotEmptyException e)
- {
- System.err.format ("%s: directory not empty%n", path);
- }
- catch (IOException e)
- {
- System.err.format ("%s: %s%n", path, e);
- }
- }
- }
InformedDelete調(diào)用Path的delete()方法解決了File的delete()方法不能確定失敗原因的問(wèn)題,當(dāng)Path的delete()當(dāng)?shù)臋z測(cè)到操作失敗時(shí),它會(huì)根據(jù)情況拋出適當(dāng)?shù)漠惓#纾?/p>
◆ 如果文件不存在,拋出java.nio.file.NoSuchFileException異常。
◆ 如果文件是一個(gè)目錄不能刪除,拋出java.nio.file.DirectoryNotEmptyException異常,因?yàn)檫@個(gè)目錄下可能還包括一個(gè)空目錄。
◆ 如果遇到其他I/O問(wèn)題,則拋出java.io.IOException的子類異常,例如,如果文件是只讀的,拋出java.nio.file.AccessDeniedException異常。
#p#
異步I/O
JSR 51引入了多路復(fù)用I/O(無(wú)阻塞I/O和選擇就緒的結(jié)合)使創(chuàng)建高可擴(kuò)展服務(wù)器變得更加容易,本質(zhì)上是這樣的,客戶端代碼用一個(gè)選擇器注冊(cè)一個(gè)套接字通道,當(dāng)通道準(zhǔn)備好可以開(kāi)始I/O操作時(shí)發(fā)出通知。
如果要深入研究多路復(fù)用I/O,請(qǐng)閱讀Ron Hitchens的《Java NIO》一書(shū)。
JSR 203還引入了異步I/O,它也被用來(lái)建立高可擴(kuò)展服務(wù)器,和多路復(fù)用I/O不同,異步I/O是讓客戶端啟動(dòng)一個(gè)I/O操作,當(dāng)操作完成后向客戶端發(fā)送一個(gè)通知。
異步I/O是通過(guò)以下位于java.nio.channels包中的接口和類實(shí)現(xiàn)的,它們的名稱前面都加了Asynchronous前綴:
◆ AsynchronousChannel – 標(biāo)識(shí)一個(gè)支持異步I/O的通道。
◆ AsynchronousByteChannel – 標(biāo)識(shí)一個(gè)支持讀寫(xiě)字節(jié)的異步通道,這個(gè)接口擴(kuò)展了AsynchronousChannel。
◆ AsynchronousDatagramChannel – 標(biāo)識(shí)一個(gè)面向數(shù)據(jù)報(bào)套接字異步通道,這個(gè)類實(shí)現(xiàn)了AsynchronousByteChannel。
◆ AsynchronousFileChannel – 標(biāo)識(shí)一個(gè)可讀,寫(xiě)和操作文件的異步通道,這個(gè)類實(shí)現(xiàn)了AsynchronousChannel。
◆ AsynchronousServerSocketChannel – 標(biāo)識(shí)一個(gè)面向流監(jiān)聽(tīng)套接字的異步通道,這個(gè)類實(shí)現(xiàn)了AsynchronousChannel。
◆ AsynchronousSocketChannel – 標(biāo)識(shí)一個(gè)面向流連接套接字的異步通道,這個(gè)類實(shí)現(xiàn)了AsynchronousByteChannel。
◆ AsynchronousChannelGroup – 標(biāo)識(shí)一個(gè)用于資源共享的異步通道組。
AsynchronousChannel文檔指定了兩種形式的異步I/O操作:
◆ Future operation(...)
◆ void operation(... A attachment, CompletionHandler handler)
operation列舉I/O操作(如讀,寫(xiě)),V是操作的結(jié)果類型,A是附加給操作的對(duì)象類型。
第一種形式需要你調(diào)用java.util.concurrent.Future方法檢查操作是否完成,等待完成和檢索結(jié)果,清單2的代碼演示了這樣一個(gè)示例。
清單2. AFCDemo1.java
- // AFCDemo1.java
- import java.io.IOException;
- import java.nio.ByteBuffer;
- import java.nio.channels.AsynchronousFileChannel;
- import java.nio.file.Path;
- import java.nio.file.Paths;
- import java.util.concurrent.Future;
- public class AFCDemo1
- {
- public static void main (String [] args) throws Exception
- {
- if (args.length != 1)
- {
- System.err.println ("usage: java AFCDemo1 path");
- return;
- }
- Path path = Paths.get (args [0]);
- AsynchronousFileChannel ch = AsynchronousFileChannel.open (path);
- ByteBuffer buf = ByteBuffer.allocate (1024);
- Future<Integer> result = ch.read (buf, 0);
- while (!result.isDone ())
- {
- System.out.println ("Sleeping...");
- Thread.sleep (500);
- }
- System.out.println ("Finished = "+result.isDone ());
- System.out.println ("Bytes read = "+result.get ());
- ch.close ();
- }
- }
調(diào)用AsynchronousFileChannel's public static AsynchronousFileChannel open(Path file, OpenOption... options)方法打開(kāi)file參數(shù)進(jìn)行讀取,然后創(chuàng)建了一個(gè)字節(jié)緩沖區(qū)存儲(chǔ)讀取操作的結(jié)果。
接下來(lái)調(diào)用public abstract Future read(ByteBuffer dst, long position)方法異步讀取文件的前1024個(gè)字節(jié),這個(gè)方法返回一個(gè)Future實(shí)例代表這個(gè)操作的結(jié)果。
調(diào)用read()方法后,進(jìn)入一個(gè)表決循環(huán),重復(fù)調(diào)用Future的isDone()方法檢查操作是否完成,一直等到讀操作結(jié)束,最后調(diào)用Future的get()方法返回讀取到的字節(jié)大小。
第二種形式需要你指定java.nio.channels.CompletionHandler,并實(shí)現(xiàn)下面的方法使用前面操作返回的結(jié)果,或是了解操作為什么失敗,并采取適當(dāng)?shù)男袆?dòng):
◆ 當(dāng)操作完成時(shí)調(diào)用void completed(V result, A attachment),這個(gè)操作的結(jié)果是由result標(biāo)識(shí)的,附加給操作的對(duì)象是由attachment標(biāo)識(shí)的。
◆ 當(dāng)操作失敗時(shí)調(diào)用void failed(Throwable exc, A attachment),操作失敗的原因是由exc標(biāo)識(shí)的,附加給操作的對(duì)象是由attachment標(biāo)識(shí)的。
#p#
我創(chuàng)建了一個(gè)程序演示創(chuàng)建和接收讀操作狀態(tài)的通知,其代碼如清單3所示。
清單3. AFCDemo2.java
- // AFCDemo2.java
- import java.io.IOException;
- import java.nio.ByteBuffer;
- import java.nio.channels.AsynchronousFileChannel;
- import java.nio.channels.CompletionHandler;
- import java.nio.file.Path;
- import java.nio.file.Paths;
- public class AFCDemo2
- {
- static Thread current;
- public static void main (String [] args) throws Exception
- {
- if (args.length != 1)
- {
- System.err.println ("usage: java AFCDemo1 path");
- return;
- }
- Path path = Paths.get (args [0]);
- AsynchronousFileChannel ch = AsynchronousFileChannel.open (path);
- ByteBuffer buf = ByteBuffer.allocate (1024);
- current = Thread.currentThread ();
- ch.read (buf, 0, null,
- new CompletionHandler<Integer, Void> ()
- {
- public void completed (Integer result, Void v)
- {
- System.out.println ("Bytes read = "+result);
- current.interrupt ();
- }
- public void failed (Throwable exc, Void v)
- {
- System.out.println ("Failure: "+exc.toString ());
- current.interrupt ();
- }
- });
- System.out.println ("Waiting for completion");
- try
- {
- current.join ();
- }
- catch (InterruptedException e)
- {
- }
- System.out.println ("Terminating");
- ch.close ();
- }
- }
上面的代碼調(diào)用AsynchronousFileChannel's public abstract void read(ByteBuffer dst, long position, A attachment, CompletionHandler handler)方法異步讀取前1024字節(jié)。
雖然我們只演示了單一的讀操作,但attachment部分也很重要,上面的代碼演示了傳遞一個(gè)null給read()方法,并指定附加類型為Void。
完整的套接字通道功能
JSR 51的DatagramChannel,ServerSocketChannel和SocketChannel類沒(méi)有完整抽象一個(gè)網(wǎng)絡(luò)套接字,為了綁定通道的套接字,或?yàn)榱双@得/設(shè)置套接字選項(xiàng),你必須先調(diào)用每個(gè)類的socket()方法檢索對(duì)等套接字。
JSR 51生效時(shí)沒(méi)有時(shí)間定義完整的套接字通道API,因此形成了套接字通道和套接字API混合的局面,JSR203引入新的java.nio.channels.NetworkChannel接口解決了這個(gè)問(wèn)題。
NetworkChannel提供了將套接字綁定到本地地址,返回綁定地址,以及獲得/設(shè)置套接字選項(xiàng)的方法,這個(gè)接口是通過(guò)同步和異步套接字類實(shí)現(xiàn)的,不再需要調(diào)用socket()方法。
JSR 203也引入了新的java.nio.channels.MulticastChannel接口,它為DatagramChannel提供了IP多播的支持,以及對(duì)應(yīng)的異步支持。
總結(jié)
本系列文章介紹了即將發(fā)布的JDK 7包含的一些新特性,新的里程碑版本可能很快就會(huì)發(fā)布,你現(xiàn)在就可以嘗試一下這些新特性,也許Oracle/Sun將會(huì)增加更多的新特性,如JWebPane瀏覽器組件,因?yàn)橹癝un就曾用閉包讓我們驚訝過(guò)一次了。
關(guān)于Java 7的更多內(nèi)容,歡迎訪問(wèn)51CTO推薦專題:Java 7 下一代Java開(kāi)發(fā)技術(shù)詳解
【JDK 7相關(guān)內(nèi)容推薦】