探索Java NIO的历程
前段时间有些时间,打算看看NIO的东西,本来以为很快可以了解的东西,却用了很多时间。
首先Goole NIO可以看到很多的教程,非阻塞,Buffer,内存映射,块读取前三个很快就有所了解
尝试着写了些小程序,学习东西的时候总喜欢写点小例子。
唯独块读取没有找到对应的东西。(在过程中,主要看了IBM 的NIO入门)
首先,IBM NIO入门中的语句
--------------------------------------------------------------------------------
原来的 I/O 库(在 java.io.*中) 与 NIO 最重要的区别是数据打包和传输的方式。正如前面提到的,
原来的 I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。 面向流 的 I/O 系统一次一个字节地处
理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易。链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。 一个 面向块 的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的 I/O 缺少一些面向流的I/O 所具有的优雅性和简单性。
--------------------------------------------------------------------------------
首先简单的印象是NIO快,所以想写个程序验证一下.如下复制:
1
public
static
void
test2(String name1, String name2)
{
2
long
start
=
System.currentTimeMillis();
3
try
{
4
FileInputStream fis
=
new
FileInputStream(name1);
5
FileOutputStream fos
=
new
FileOutputStream(name2);
6
byte
[] buf
=
new
byte
[
8129
];
7
while
(
true
)
{
8
int
n
=
fis.read(buf);
9
if
(n
==
-
1
)
{
10
break
;
11
}
12
fos.write(buf,
0
,n);
13
}
14
fis.close();
15
fos.close();
16
}
catch
(Exception e)
{
17
e.printStackTrace();
18
}
19
long
end
=
System.currentTimeMillis();
20
long
time
=
end
-
start;
21
System.out.println(time);
22
}
23
24
public
static
void
test3(String name1, String name2)
{
25
long
start
=
System.currentTimeMillis();
26
try
{
27
FileInputStream in
=
new
FileInputStream(name1);
28
FileOutputStream out
=
new
FileOutputStream(name2);
29
FileChannel fc1
=
in.getChannel();
30
FileChannel fc2
=
out.getChannel();
31
ByteBuffer bb
=
ByteBuffer.allocate(
8129
);
32
while
(
true
)
{
33
bb.clear();
34
int
n
=
fc1.read(bb);
35
if
(n
==
-
1
)
{
36
break
;
37
}
38
bb.flip();
39
fc2.write(bb);
40
}
41
fc1.close();
42
fc2.close();
43
}
catch
(IOException e)
{
44
45
}
46
long
end
=
System.currentTimeMillis();
47
long
time
=
end
-
start;
48
System.out.println(time);
49
}
本以为可以结束,结果测试结果出乎意料,函数一比函数二要快,就是说Old IO快于NIO ,从此
也就开始了整个过程:
为了了解这个问题,仔细搜索并仔细再看IBM 的NIO教程,看到如下这段话
---------------------------------------------
在 JDK 1.4 中原来的 I/O 包和 NIO 已经很好地集成了。 java.io.* 已经以 NIO 为基础重新实现了,
所以现在它可以利用 NIO 的一些特性。例如, java.io.* 包中的一些类包含以块的形式读写数据的方法,
这使得即使在更面向流的系统中,处理速度也会更快。 也可以用 NIO 库实现标准 I/O 功能。例如,
可以容易地使用块 I/O 一次一个字节地移动数据。但是正如您会看到的,NIO 还提供了原 I/O 包中所没有的许多好处。
---------------------------------------------
所以我想,是否因为InputStream中使用了块读取实现了呢,所以进入JDK1.4中的InputStream中
看看source,首先引起我注意的是read函数,当参数是一个byte数组的时候,直接调用的native实现
难道是这个,为了验证,下载了一个JDK1.3下来,发现JDK1.3是一样的。
继续,我想是否是JVM底层实现了块读取呢,为了证明这个我用JDK1.3和JDK1.4同时实现了类似的函数, 测试的结果再次出乎意料,性能相差不大.那就不是这个了。
为此多方查找资料,未果,为此多写几个函数,好好测试一下IO的不同。于是有了如下的一些函数
1
//
exec
2
public
static
void
test1(String name1, String name2)
{
3
long
start
=
System.currentTimeMillis();
4
try
{
5
String cmd
=
"
cmd /c copy d:\\out1.txt d:\\out2.txt
"
;
6
System.out.println(cmd);
7
Process p
=
Runtime.getRuntime().exec(cmd);÷
8
p.waitFor();
9
}
catch
(Exception e)
{
10
e.printStackTrace();
11
}
12
long
end
=
System.currentTimeMillis();
13
long
time
=
end
-
start;
14
System.out.println(time);
15
}
16
17
//
old io
18
public
static
void
test2(String name1, String name2)
{
19
long
start
=
System.currentTimeMillis();
20
try
{
21
FileInputStream fis
=
new
FileInputStream(name1);
22
FileOutputStream fos
=
new
FileOutputStream(name2);
23
while
(
true
)
{
24
byte
[] buf
=
new
byte
[
8129
];
25
int
n
=
fis.read(buf);
26
if
(n
==
-
1
)
{
27
break
;
28
}
29
fos.write(buf);
30
}
31
fis.close();
32
fos.close();
33
}
catch
(Exception e)
{
34
e.printStackTrace();
35
}
36
long
end
=
System.currentTimeMillis();
37
long
time
=
end
-
start;
38
System.out.println(time);
39
}
40
41
//
new io
42
public
static
void
test3(String name1, String name2)
{
43
long
start
=
System.currentTimeMillis();
44
try
{
45
FileInputStream in
=
new
FileInputStream(name1);
46
FileOutputStream out
=
new
FileOutputStream(name2);
47
FileChannel fc1
=
in.getChannel();
48
FileChannel fc2
=
out.getChannel();
49
ByteBuffer bb
=
ByteBuffer.allocate(
8129
);
50
while
(
true
)
{
51
bb.clear();
52
int
n
=
fc1.read(bb);
53
if
(n
==
-
1
)
{
54
break
;
55
}
56
bb.flip();
57
fc2.write(bb);
58
}
59
fc1.close();
60
fc2.close();
61
}
catch
(IOException e)
{
62
63
}
64
long
end
=
System.currentTimeMillis();
65
long
time
=
end
-
start;
66
System.out.println(time);
67
}
68
69
//
fast copy
70
public
static
void
test4(String name1, String name2)
{
71
long
start
=
System.currentTimeMillis();
72
try
{
73
FileInputStream in
=
new
FileInputStream(name1);
74
;
75
FileOutputStream out
=
new
FileOutputStream(name2);
76
;
77
FileChannel fc1
=
in.getChannel();
78
FileChannel fc2
=
out.getChannel();
79
ByteBuffer bb
=
ByteBuffer.allocateDirect(
8129
);
80
while
(
true
)
{
81
bb.clear();
82
int
n
=
fc1.read(bb);
83
if
(n
==
-
1
)
{
84
break
;
85
}
86
bb.flip();
87
fc2.write(bb);
88
}
89
fc1.close();
90
fc2.close();
91
}
catch
(IOException e)
{
92
93
}
94
long
end
=
System.currentTimeMillis();
95
long
time
=
end
-
start;
96
System.out.println(time);
97
}
98
99
//
transfer ,read and write at same time
100
public
static
void
test5(String name1, String name2)
{
101
long
start
=
System.currentTimeMillis();
102
try
{
103
RandomAccessFile raf1
=
new
RandomAccessFile(name1,
"
rw
"
);
104
RandomAccessFile raf2
=
new
RandomAccessFile(name2,
"
rw
"
);
105
FileChannel fc1
=
raf1.getChannel();
106
FileChannel fc2
=
raf2.getChannel();
107
fc1.transferTo(
0
, raf1.length(), fc2);
108
fc1.close();
109
fc2.close();
110
}
catch
(Exception e)
{
111
e.printStackTrace();
112
}
113
long
end
=
System.currentTimeMillis();
114
long
time
=
end
-
start;
115
System.out.println(time);
116
}
首先测试的文件是一个30几M的文件,测试结果出乎意料,是否因为文件太小呢 ?用个200M的文件再次
测试一下,结果更匪夷所思,transfor的方式快些,exec的方式快些,Old IO和NIO相差不大,而且
稍快与NIO,拿到这个结果真让人气馁,再次查找资料,但都没有解决自己的疑问,这个时候看到了
Think In Java第三版上讲到NIO,更让我兴奋的是我看到如下的话,大概意思吧,原文记不住了
"Java NIO在文件读取和网络方面有很大的性能提高,网络方面不说,这里说说文件操作"看到这段话
真是高兴,因为Think In Java一向都是有很多写的很好的小例子,怀着这种心情,往下看,终于找到了
一个显示性能差别小例子,但让我哭笑不得的是,作者居然是使用的内存映射的例子。类似这样
RandomAccessFile raf1 = new RandomAccessFile(name1, "rw");
FileChannel fc1 = raf1.getChannel();
MappedByteBuffer mbb = fc1.map(FileChannel.MapMode.READ_WRITE, 0,
1024);
使用内存映射来和传统的IO来对比读写,本来就是个不公平的事情。内存映射的读取实际是内存读写
和传统IO比肯定有很大差距。到现在,从开始NIO到现在已经有1周的时间了,每天我都会在工作之余拿出 1-2个小时看看NIO,但这个问题越来越迷离。
今天有时间把这个过程写出来,一方面感觉过程无奈,写出来留个纪念,另一方面希望和大家交流一下, 如果你没看过NIO或者没有作过这个方面的尝试,如果你有兴趣可以一起探讨,如果你了解这个问题,请 不吝赐教,或给个提示,或者告诉我我的测试错在什么地方,不胜感谢。同时,我会继续这个问题,直到 找到答案。然后会把答案放上来共享。
# re: 探索Java NIO的历程 2006-11-19 15:49
NIO的目的不是在单个IO的时候快过普通I/O,而是在并发多个I/O的时候减少线程的使用量,从而提高整个系统的可伸缩性。 回复 更多评论
# re: 探索Java NIO的历程 2006-11-19 19:34
NIO的目的是加速IO,并发多个IO时候减少线程使用量是其中的一个很重要的部分,这不排除,文章开始我已经说了。但是在验证过程中,其它的问题例如非阻塞,例如内存映射,有大量的例子,很容易让人理解,但我提到的问题,并没有找到答案。我的意思不是说这个重要,而是疑问在这里。 回复 更多评论
# re: 探索Java NIO的历程 2006-11-19 21:37
看了两遍,还是不清楚你的问题是啥,呵呵。 回复 更多评论
# re: 探索Java NIO的历程 2006-11-19 21:44
谢谢你指出问题,也许我说的不够清楚.
我的疑问是:
很多地方(IBM NIO入门教程,Think In Java, google到的教材)都说NIO的文件操作有很大幅度的性能提高.但是我自己写程序证明不出什么地方提高了,我还没找到方法可以证明性能提高了。我学习东西的一个习惯是写一些小的例子,证明事实确实如此,但块读取提高性能方面,我确实没有做到,写出的程序要不互相矛盾,要不是反例。
回复 更多评论
# re: 探索Java NIO的历程 2006-11-19 21:59
其实读取文件,除非JDK代码有问题,否则怎么也不会有大幅性能提升了。读文件,完全是磁盘性能问题了。
下面这段话(来自Java™ I/O, 2nd Edition)也许能解决你部分问题:
Nonblocking I/O is primarily relevant to network connections. Pipe channels that move data between two threads also support nonblocking I/O. File channels don't support it at all because file access doesn't block nearly as often as network channels do, and most modern disk controllers can fill a CPU with data fast enough to keep it satisfied. Furthermore, it's uncommon for one program to read or write hundreds of files simultaneously. However, on network servers, this usage pattern is the rule, not the exception.
回复 更多评论
# re: 探索Java NIO的历程 2006-11-19 22:08
首先,谢谢你帮忙解决我的疑问。:)
然后,这个问题如果是这样的话,所有的现象就说的通了,我再看看。谢谢 回复 更多评论
# re: 探索Java NIO的历程 2006-11-20 10:20
JDK1.4以上的IO读取底层使用的实际上都是NIO,要比较得拿1.3里的OLD IO去比较。 回复 更多评论
# re: 探索Java NIO的历程 2006-11-20 14:18
楼上看我的文章,我确是拿jdk1.3比较了。 回复 更多评论
# re: 探索Java NIO的历程 2006-11-28 11:09
呵呵,你一定是没有用对jdk吧我把你上面方法的2,3,4在jdk1.5的环境中测试拷贝一个20M的文件,时间,分别是297,453,406,不过方法2在jdk1.3的环境中用时却是3969。 回复 更多评论
# re: 探索Java NIO的历程2006-11-28 11:37
首先jdk的使用是肯定没错的,我测试了很多次,而且反复确认了这个问题。你的这个数据我测试出来过,首先说说一些测试时候的注意的几个问题吧。
1,不能直接一个main函数调用所有的test(),因为会有内存回收的问题。可以尝试把执行顺序倒过来,会发现所有的测试数据会有大变化
2,可以在每个函数之间加上System.gc(),但是依然会有问题,大概的规律是第一次执行慢,第二次快,以后还有快的几次,然後忽然又变慢。这个不是虚拟机load的问题。
3,可以做如下测试,只执行一个函数,用手动执行5-7次,点的快和慢得到的结果很大差距。
4,建议用大一点的文件200m,或者更大,因为这样可以渐少一些特殊因素的影响
5,多次测试,去掉怪异的数值,平均值
6,这个问题我现在发现的最稳妥的办法是,执行test1(),等半分钟,再执行另一个,虽然这个方法是笨的不行,但是确是得出的数据稳定些,其他的办法很难得出
正确的数据。
7,我也尝试了在linux来测试,效果也不明显。 回复 更多评论