Java NIO(New Input/Output)——新的輸入/輸出API包——是2002年引入到J2SE 1.4裏的。Java NIO的目標是提升Java平臺上的I/O密集型任務的性能。過了十年,不少Java開發者仍是不知道怎麼充分利用NIO,更少的人知道在Java SE 7裏引入了更新的輸入/輸出 API(NIO.2)。這篇教程展現了5個在Java編程的一些常見場景裏使用NIO和NIO.2包的簡單示例。html
NIO和NIO.2對於Java平臺最大的貢獻是提升了Java應用開發中的一個核心組件的性能:輸入/輸出處理。不過這兩個包都不是很好用,而且它們也不是適用於全部的場景。若是可以正確地使用的話,Java NIO和NIO.2能夠大大減小一些經常使用I/O操做所花的時間。這就是NIO和NIO.2所具備的超能力,我會在這篇文章裏向你展現5種使用它們的簡單方式。
java
NIO的背景程序員
爲何一個已經存在10年的加強包仍是Java的新I/O包呢?緣由是對於大多數的Java程序員而言,基本的I/O操做都可以勝任。在平常工做中,大部分的Java開發者沒有必要去學習NIO。更進一步,NIO不只僅是一個性能提高包。相反,它是一個和Java I/O相關的不一樣功能的集合。NIO經過使得Java應用的性能「更加接近實質」來達到性能提高的效果,也就是意味着NIO和NIO.2的API暴露了低層次的系統操做的入口。NIO的代價就是它在提供更強大的I/O控制能力的同時,也要求咱們比使用基本的I/O編程更加細心地使用和練習。NIO的另外一特色是它對於應用程序的表現力的關注,這個咱們會在下面的練習中看到。編程
開始學習NIO和NIO.2小程序
NIO的參考資料很是多——參考資料中選取的一些連接。要學習NIO和NIO.2的話,Java 2 SDK Standard Edition(SE) documentation 和 Java SE 7 documentation 都是不可或缺的。要使用這篇文章裏的代碼,你須要使用JDK 7或者更高的版本。網絡
對於不少開發者而言,它們第一次遇到NIO均可能是在維護應用的時候:一個功能正常的應用響應愈來愈慢,所以有人建議使用NIO來提升響應速度。NIO在提高應用性能的時候顯得比較出衆,不過具體的結果取決於底層系統.(注意NIO是平臺相關的)。若是你是第一次使用NIO的話,你須要仔細衡量。你會發現NIO提高性能的能力不只僅取決於OS,同時也取決於你所使用的JVM,主機的虛擬上下文,大容量存儲的特性甚至和數據也是相關的。所以,性能衡量的工做是比較難作的。尤爲是當你的系統存在一個可移動的部署環境的時候,你須要特別注意。app
瞭解了上面的內容後,咱們沒有後顧之憂了,如今就來體驗一下NIO和NIO.2的5個重要的功能。dom
1. 變動通知(由於每一個事件都須要一個監聽者)異步
對NIO和NIO.2有興趣的開發者的共同關注點在於Java應用的性能。根據個人經驗,NIO.2裏的文件變動通知者(file change notifier)是新輸入/輸出API裏最讓人感興趣(被低估了)的特性。socket
不少企業級應用須要在下面的狀況時作一些特殊的處理:
這些都是變動通知或者變動響應的例子。在Java(以及其餘語言)的早期版本里,輪詢(polling)是檢測這些變動事件的最好方式。輪詢是一種特殊的無限循環:檢查文件系統或者其餘對象,而且和以前的狀態對比,若是沒有變化,在大概幾百個毫秒或者10秒的間隔後,繼續檢查。就這一直無限循環下去。
NIO.2提供了一個更好地方式來進行變動檢測。列表1是一個簡單的示例。
列表1. NIO.2裏的變動通知機制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
import
java.nio.file.attribute.*;
import
java.io.*;
import
java.util.*;
import
java.nio.file.Path;
import
java.nio.file.Paths;
import
java.nio.file.StandardWatchEventKinds;
import
java.nio.file.WatchEvent;
import
java.nio.file.WatchKey;
import
java.nio.file.WatchService;
import
java.util.List;
public
class
Watcher {
public
static
void
main(String[] args) {
Path this_dir = Paths.get(
"."
);
System.out.println(
"Now watching the current directory ..."
);
try
{
WatchService watcher = this_dir.getFileSystem().newWatchService();
this_dir.register(watcher, StandardWatchEventKinds.ENTRY_CREATE);
WatchKey watckKey = watcher.take();
List<WatchEvent<<
64
;>> events = watckKey.pollEvents();
for
(WatchEvent event : events) {
System.out.println(
"Someone just created the file '"
+ event.context().toString() +
"'."
);
}
}
catch
(Exception e) {
System.out.println(
"Error: "
+ e.toString());
}
}
}
|
編譯這段代碼,而後在命令行裏執行。在相同的目錄下,建立一個新的文件,例如運行touch example或者copy Watcher.class example命令。你會看到下面的變動通知消息:
Someone just create the fiel ‘example1′.
這個簡單的示例展現了怎麼開始使用Java NIO的功能。同時,它也介紹了NIO.2的Watcher類,它相比較原始的I/O中的輪詢方案而言,顯得更加直接和易用。
注意拼寫錯誤
當你從這篇文章裏拷貝代碼時,注意拼寫錯誤。例如,列表1種的StandardWatchEventKinds 對象是複數的形式。即便在Java.net的文檔裏都把它給拼寫錯了。
小技巧
NIO裏的通知機制比老的輪詢方式使用起來更加簡單,這樣會誘導你忽略對具體需求的詳細分析。當你在你第一次使用一個監聽器的時候,你須要仔細考慮你所使用的這些概念的語義。例如,知道一個變動何時會結束比知道它何時開始更加劇要。這種分析須要很是仔細,尤爲是像移動FTP文件夾這種常見的場景。NIO是一個功能很是強大的包,但同時它還會有一些微妙的「陷阱」,這會給那些不熟悉它的人帶來困擾。
2. 選擇器和異步IO:經過選擇器來提升多路複用
NIO新手通常都把它和「非阻塞輸入/輸出」聯繫在一塊兒。NIO不只僅只是非阻塞I/O,不過這種認知也不徹底是錯的:Java的基本I/O是阻塞式I/O——意味着它會一直等待到操做完成——然而,非阻塞或者異步I/O是NIO裏最經常使用的一個特色,而非NIO的所有。
NIO的非阻塞I/O是事件驅動的,而且在列表1裏文件系統監聽示例裏進行了展現。這就意味着給一個I/O通道定義一個選擇器(回調或者監聽器),而後程序能夠繼續運行。當一個事件發生在這個選擇器上時——例如接收到一行輸入——選擇器會「醒來」而且執行。全部的這些都是經過一個單線程來實現的,這和Java的標準I/O有着顯著的差異的。
列表2裏展現了使用NIO的選擇器實現的一個多端口的網絡程序echo-er,這裏是修改了Greg Travis在2003年建立的一個小程序(參考資源列表)。Unix和類Unix系統很早就已經實現高效的選擇器,它是Java網絡高性能編程模型的一個很好的參考模型。
列表2. NIO選擇器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
import
java.io.*;
import
java.net.*;
import
java.nio.*;
import
java.nio.channels.*;
import
java.util.*;
public
class
MultiPortEcho
{
private
int
ports[];
private
ByteBuffer echoBuffer = ByteBuffer.allocate(
1024
);
public
MultiPortEcho(
int
ports[] )
throws
IOException {
this
.ports = ports;
configure_selector();
}
private
void
configure_selector()
throws
IOException {
// Create a new selector
Selector selector = Selector.open();
// Open a listener on each port, and register each one
// with the selector
for
(
int
i=
0
; i<ports.length; ++i) {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(
false
);
ServerSocket ss = ssc.socket();
InetSocketAddress address =
new
InetSocketAddress(ports[i]);
ss.bind(address);
SelectionKey key = ssc.register(selector, SelectionKey.OP_ACCEPT);
System.out.println(
"Going to listen on "
+ ports[i]);
}
while
(
true
) {
int
num = selector.select();
Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();
while
(it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
if
((key.readyOps() & SelectionKey.OP_ACCEPT)
== SelectionKey.OP_ACCEPT) {
// Accept the new connection
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(
false
);
// Add the new connection to the selector
SelectionKey newKey = sc.register(selector, SelectionKey.OP_READ);
it.remove();
System.out.println(
"Got connection from "
+sc );
}
else
if
((key.readyOps() & SelectionKey.OP_READ)
== SelectionKey.OP_READ) {
// Read the data
SocketChannel sc = (SocketChannel)key.channel();
// Echo data
int
bytesEchoed =
0
;
while
(
true
) {
echoBuffer.clear();
int
number_of_bytes = sc.read(echoBuffer);
if
(number_of_bytes <=
0
) {
break
;
}
echoBuffer.flip();
sc.write(echoBuffer);
bytesEchoed += number_of_bytes;
}
System.out.println(
"Echoed "
+ bytesEchoed +
" from "
+ sc);
it.remove();
}
}
}
}
static
public
void
main( String args[] )
throws
Exception {
if
(args.length<=
0
) {
System.err.println(
"Usage: java MultiPortEcho port [port port ...]"
);
System.exit(
1
);
}
int
ports[] =
new
int
[args.length];
for
(
int
i=
0
; i<args.length; ++i) {
ports[i] = Integer.parseInt(args[i]);
}
new
MultiPortEcho(ports);
}
}
|
編譯這段代碼,而後經過相似於java MultiPortEcho 8005 8006這樣的命令來啓動它。一旦這個程序運行成功,啓動一個簡單的telnet或者其餘的終端模擬器來鏈接8005和8006接口。你會看到這個程序會回顯它接收到的全部字符——而且它是經過一個Java線程來實現的。
3. 通道:承諾與現實
在NIO裏,一個通道(channel)能夠表示任何能夠讀寫的對象。它的做用是爲文件和套接口提供抽象。NIO通道支持一系列一致的方法,這樣就使得編碼的時候不須要去特別關心不一樣的對象,不管它是標準輸出,網絡鏈接仍是正在使用的通道。通道的這個特性是繼承自Java基本I/O中的流(stream)。流(stream)提供了阻塞式的IO;通道支持異步I/O。
NIO常常會由於它的性能高而被推薦,不過更準確地是由於它的響應快速。在有些場景下NIO會比基本的Java I/O的性能要差。例如,對於一個小文件的簡單的順序讀寫,簡單經過流來實現的性能可能比對應的面向事件的基於通道的編碼實現的快兩到三倍。同時,非多路複用(non-multiplex)的通道——也就是每一個線程一個單獨的通道——要比多個通道把各自的選擇器註冊在同一個線程裏要慢多了。
下面你在考慮是使用流仍是通道的時候,試着問本身下面幾個問題:
這樣的分析是決定使用流仍是通道的一個最佳實踐。記住:NIO和NIO.2不是基本I/O的替代,而它的一個補充。
4. 內存映射——好鋼用在刀刃上
NIO裏對性能提高最顯著的是內存映射(memory mapping)。內存映射是一個系統層面的服務,它把程序裏用到的文件的一段看成內存來處理。
內存映射存在不少潛在的影響,比我這裏提供的要多。在一個更高的層次上,它可以使得文件訪問的I/O的性能達到內存訪問的速度。內存訪問的速度每每比文件訪問的速度快幾個數量級。列表3是一個NIO內存映射的一個簡單示例。
列表3. NIO裏的內存映射
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
import
java.io.RandomAccessFile;
import
java.nio.MappedByteBuffer;
import
java.nio.channels.FileChannel;
public
class
mem_map_example {
private
static
int
mem_map_size =
20
*
1024
*
1024
;
private
static
String fn =
"example_memory_mapped_file.txt"
;
public
static
void
main(String[] args)
throws
Exception {
RandomAccessFile memoryMappedFile =
new
RandomAccessFile(fn,
"rw"
);
//Mapping a file into memory
MappedByteBuffer out = memoryMappedFile.getChannel().map(FileChannel.MapMode.READ_WRITE,
0
, mem_map_size);
//Writing into Memory Mapped File
for
(
int
i =
0
; i < mem_map_size; i++) {
out.put((
byte
)
'A'
);
}
System.out.println(
"File '"
+ fn +
"' is now "
+ Integer.toString(mem_map_size) +
" bytes full."
);
// Read from memory-mapped file.
for
(
int
i =
0
; i <
30
; i++) {
System.out.print((
char
) out.get(i));
}
System.out.println(
"\nReading from memory-mapped file '"
+ fn +
"' is complete."
);
}
}
|
在列表3中,這個簡單的示例建立了一個20M的文件example_memory_mapped_file.txt,而且用字符A對它進行填充,而後讀取前30個字節。在實際的應用中,內存映射不只僅擅長提升I/O的原始速度,同時它也容許多個不一樣的reader和writer同時處理同一個文件鏡像。這個技術功能強大可是也很危險,不過若是正確使用的話,它會使得你的IO速度提升數倍。衆所周知,華爾街的交易操做爲了可以贏得秒級甚至是毫秒級的優點,都使用了內存映射技術。
5. 字符編碼和搜索
我在這篇文章裏要講解的NIO的最後一個特性是charset,一個用來轉換不一樣字符編碼的包。在NIO以前,Java經過getByte方法內置實現了大部分相同的功能。charset很受歡迎,由於它比getBytes更加靈活,而且可以在更底層去實現,這樣就可以得到更好的性能。這個對於搜索那些對於編碼、順序以及其餘語言特色比較敏感的非英語語言而言更加有價值。
列表4展現了一個把Java裏的Unicode字符轉換成Latin-1的示例
列表4. NIO裏的字符
1
2
3
4
|
String some_string =
"This is a string that Java natively stores as Unicode."
;
Charset latin1_charset = Charset.forName(
"ISO-8859-1"
);
CharsetEncode latin1_encoder = charset.newEncoder();
ByteBuffer latin1_bbuf = latin1_encoder.encode(CharBuffer.wrap(some_string));
|
注意Charset和通道被設計成可以放在一塊兒進行使用,這樣就可以使得程序在內存映射、異步I/O以及編碼轉換進行協做的時候,可以正常運行。
總結:固然還有更多須要去了解
這篇文章的目的是爲了讓Java開發者可以熟悉NIO和NIO.2裏的一些最主要(也是最有用)的功能。你能夠經過這些示例創建起來的一些基礎來理解NIO的一些其餘方法;例如,你所學習的關於通道的知識可以幫助你去理解NIO的Path裏對於文件系統裏的符號連接的處理。你也能夠參考一下我後面給出的資源列表,裏面給出了一些深刻學習Java新I/O API的文檔。