java nio学习

最近一直在研究java nio,提出一点浅见,希望能和大家分享!!!

废话不多说了,直接进入主题!

首先了解下所谓的java nio是个什么东西!

 

传统的并发型服务器设计是利用阻塞型网络I/O 以多线程的模式来实现的,然而由
于系统常常在进行网络读写时处于阻塞状态,会大大影响系统的性能;自Java1. 4 开始引入
了NIO(新I/O) API,通过使用非阻塞型I/O,实现流畅的网络读写操作,为开发高性能并发
型服务器程序提供了一个很好的解决方案。这就是java nio

 

首先来看下传统的阻塞型网络 I/O的不足

Java 平台传统的I/O 系统都是基于Byte(字节)和Stream(数据流)的,相应的I/O 操
作都是阻塞型的,所以服务器程序也采用阻塞型I/O 进行数据的读、写操作。本文以TCP
长连接模式来讨论并发型服务器的相关设计,为了实现服务器程序的并发性要求,系统由一
个单独的主线程来监听用户发起的连接请求,一直处于阻塞状态;当有用户连接请求到来时,
程序都会启一个新的线程来统一处理用户数据的读、写操作。

 

这种模式的优点是简单、实用、易管理;然而缺点也是显而易见的:由于是为每一个客
户端分配一个线程来处理输入、输出数据,其线程与客户机的比例近似为1:1,随着线程
数量的不断增加,服务器启动了大量的并发线程,会大大加大系统对线程的管理开销,这将
成为吞吐量瓶颈的主要原因;其次由于底层的I/O 操作采用的同步模式,I/O 操作的阻塞管

理粒度是以服务于请求的线程为单位的,有可能大量的线程会闲置,处于盲等状态,造成I/O
资源利用率不高,影响整个系统的性能。


对于并发型服务器,系统用在阻塞型I/O 等待和线程间切换的时间远远多于CPU 在内
存中处理数据的时间,因此传统的阻塞型I/O 已经成为制约系统性能的瓶颈。Java1.4 版本
后推出的NIO 工具包,提供了非阻塞型I/O 的异步输入输出机制,为提高系统的性能提供
了可实现的基础机制。

 

NIO 包及工作原理
针对传统I/O 工作模式的不足,NIO 工具包提出了基于Buffer(缓冲区)、Channel(通
道)、Selector(选择器)的新模式;Selector(选择器)、可选择的Channel(通道)和
SelectionKey(选择键)配合起来使用,可以实现并发的非阻塞型I/O 能力。

 

NIO 工具包的成员
 Buffer
(缓冲器)
Buffer 类是一个抽象类,它有7 个子类分别对应于七种基本的数据类型:ByteBuffer、
CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer 和ShortBuffer。每一个Buffer
对象相当于一个数据容器,可以把它看作内存中的一个大的数组,用来存储和提取所有基本

类型(boolean 型除外)的数据。Buffer 类的核心是一块内存区,可以直接对其执行与内存有关
的操作,利用操作系统特性和能力提高和改善Java 传统I/O 的性能。


 Channel
(通道)
Channel 
被认为是NIO 工具包的一大创新点,是(Buffer)缓冲器和I/O 服务之间的通道,
具有双向性,既可以读入也可以写出,可以更高效的传递数据。我们这里主要讨论
ServerSocketChannel 和SocketChannel,它们都继承了SelectableChannel,是可选择的通道,
分别可以工作在同步和异步两种方式下(这里的可选择不是指可以选择两种工作方式,而是
指可以有选择的注册自己感兴趣的事件)。当通道工作在同步方式时,它的功能和编程方法
与传统的ServerSocket、Socket 对象相似;当通道工作在异步工作方式时,进行输入输出处
理不必等到输入输出完毕才返回,并且可以将其感兴趣的(如:接受操作、连接操作、读出
操作、写入操作)事件注册到Selector 对象上,与Selector 对象协同工作可以更有效率的支

持和管理并发的网络套接字连接。


Selector
(选择器)和SelectionKey(选择键)
各类 Buffer 是数据的容器对象;各类Channel 实现在各类Buffer 与各类I/O 服务间传输
数据。Selector 是实现并发型非阻塞I/O 的核心,各种可选择的通道将其感兴趣的事件注册
到Selector 对象上,Selector 在一个循环中不断轮循监视这各些注册在其上的Socket 通道。
SelectionKey 类则封装了SelectableChannel 对象在Selector 中的注册信息。当Selector 监测
到在某个注册的SelectableChannel 上发生了感兴趣的事件时,自动激活产生一个SelectionKey
对象,在这个对象中记录了哪一个SelectableChannel 上发生了哪种事件,通过对被激活的
SelectionKey 的分析,外界可以知道每个SelectableChannel 发生的具体事件类型,进行相应的

处理。

 

NIO 工作原理
通过上面的讨论,我们可以看出在并发型服务器程序中使用NIO,实际上是通过网络事
件驱动模型实现的。我们应用Select 机制,不用为每一个客户端连接新启线程处理,而是将
其注册到特定的Selector 对象上,这就可以在单线程中利用Selector 对象管理大量并发的网
络连接,更好的利用了系统资源;采用非阻塞I/O 的通信方式,不要求阻塞等待I/O 操作完
成即可返回,从而减少了管理I/O 连接导致的系统开销,大幅度提高了系统性能。

 

当有读或写等任何注册的事件发生时,可以从Selector 中获得相应的
SelectionKey , 从SelectionKey 中可以找到发生的事件和该事件所发生的具体的
SelectableChannel,以获得客户端发送过来的数据。由于在非阻塞网络I/O 中采用了事件触
发机制,处理程序可以得到系统的主动通知,从而可以实现底层网络I/O 无阻塞、流畅地读
写,而不像在原来的阻塞模式下处理程序需要不断循环等待。使用NIO,可以编写出性能更
好、更易扩展的并发型服务器程序。

 

并发型服务器程序的实现代码


应用 NIO 工具包,基于非阻塞网络I/O 设计的并发型服务器程序与以往基于阻塞I/O 的
实现程序有很大不同,在使用非阻塞网络I/O 的情况下,程序读取数据和写入数据的时机不
是由程序员控制的,而是Selector 决定的。下面便给出基于非阻塞网络I/O 的并发型服务器
程序的核心代码片段:

import java.io.*; //引入Java.io包
import java.net.*; //引入Java.net包
import java.nio.channels.*; //引入Java.nio.channels包
import java.util.*; //引入Java.util包
public class TestServer implements Runnable

{
/**

服务器Channel对象,负责接受用户连接
*/
private ServerSocketChannel server

/**
* Selector
对象,负责监控所有的连接到服务器的网络事件的发生
*/

private Selector selector

/**
总的活动连接数
*/

private int activeSockets

/**
服务器Channel绑定的端口号
*/
private int port 

/**
*
构造函数
*/
public TestServer()throws IOException
{
activeSockets=0

port=9999
//初始化服务器Channel绑定的端口号为9999
selector= Selector.open()//初始化Selector对象
server=ServerSocketChannel.open()//初始化服务器Channel对象
ServerSocket socket=server.socket()
//获取服务器Channel对应的//ServerSocket对象
socket.bind(new InetSocketAddress(port))//把Socket绑定到监听端口9999上
server.configureBlocking(false)//将服务器Channel设置为非阻塞模式
server.register(selector,SelectionKey.OP_ACCEPT)

//将服务器Channel注册到Selector对象,并指出服务器Channel所感兴趣的事件为可接受请求操作
}
public void run()
{
while(true)
{
try
{
/**
*
应用Select机制轮循是否有用户感兴趣的新的网络事件发生,当没有

新的网络事件发生时,此方法会阻塞,直到有新的网络事件发生为止
*/
selector.select()

}
catch(IOException e)
{
continue
//当有异常发生时,继续进行循环操作
}
/**
得到活动的网络连接选择键的集合
*/
Set<SelectionKey> keys=selector.selectedKeys()
activeSockets=keys.size()
//获取活动连接的数目
if(activeSockets==0)
{
continue
//如果连接数为0,则继续进行循环操作
}
/**

/**
应用For—Each循环遍历整个选择键集合
*/
for(SelectionKey key :keys)
{
/**
如果关键字状态是为可接受,则接受连接,注册通道,以接受更多的*
事件,进行相关的服务器程序处理
*/
if(key.isAcceptable())
{
doServerSocketEvent(key)

continue

}
/**
如果关键字状态为可读,则说明Channel是一个客户端的连接通道,
* 进行相应的读取客户端数据的操作
*/
if(key.isReadable())
{
doClientReadEvent(key)

continue

}
/**
如果关键字状态为可写,则也说明Channel是一个客户端的连接通道,
* 进行相应的向客户端写数据的操作
*/
if(key.isWritable())
{
doClinetWriteEvent(key)

continue

}
}
}
}

/**
处理服务器事件操作
* @param key 
服务器选择键对象
*/
private void doServerSocketEvent(SelectionKey key)
{
SocketChannel client=null

try
{
ServerSocketChannel server=(ServerSocketChannel)key.channel()

client=server.accept();
if(client==null)
{
return;
}
client.configureBlocking(false);//将客户端Channel设置为非阻塞型
/**

/**
将客户端Channel注册到Selector对象上,并且指出客户端Channel所感
* 兴趣的事件为可读和可写
*/
client.register(selector,SelectionKey.OP_READ|SelectionKey.OP_READ)
}catch(IOException e)
{
try
{
client.close()

}catch(IOException e1){}
}
}
/**
进行向客户端写数据操作
* @param key 
客户端选择键对象
*/
private void doClinetWriteEvent(SelectionKey key)
{
代码实现略;
}
/**
进行读取客户短数据操作
* @param key 
客户端选择键对象
*/
private void doClientReadEvent(SelectionKey key)
{
代码实现略;
}
}

从上面对代码可以看出,使用非阻塞性I/O进行并发型服务器程序设计分三个部分:1.
向Selector对象注册感兴趣的事件;2.从Selector中获取所感兴趣的事件;3.根据不同的事件进
行相应的处理。


结语

通过使用NIO 工具包进行并发型服务器程序设计,一个或者很少几个Socket 线程就可
以处理成千上万个活动的Socket 连接,大大降低了服务器端程序的开销;同时网络I/O 采取
非阻塞模式,线程不再在读或写时阻塞,操作系统可以更流畅的读写数据并可以更有效地向
CPU 传递数据进行处理,以便更有效地提高系统的性能。

 

看到这里相信你看了不止30分钟了吧,  我说30分钟其实就是想让大家能够轻松的读下去(鸡蛋。。。)

 

好了,到这里大家应该对java nio有个初步的了解了吧~~~

 

下次给大家带来一个java nio开发的实例吧~~

 

Java NIO类库Selector机制解析(上)

一、  前言

 

自从J2SE 1.4版本以来,JDK发布了全新的I/O类库,简称NIO,其不但引入了全新的高效的I/O机制,同时,也引入了多路复用的异步模式。NIO的包中主要包含了这样几种抽象数据类型:

 

·                Buffer:包含数据且用于读写的线形表结构。其中还提供了一个特殊类用于内存映射文件的I/O操作。

·                Charset:它提供Unicode字符串影射到字节序列以及逆映射的操作。

·                Channels:包含socket,file和pipe三种管道,都是全双工的通道。

·                Selector:多个异步I/O操作集中到一个或多个线程中(可以被看成是Unix中select()函数的面向对象版本)。

 

我的大学同学赵锟在使用NIO类库书写相关网络程序的时候,发现了一些Java异常RuntimeException,异常的报错信息让他开始了对NIO的Selector进行了一些调查。当赵锟对我共享了Selector的一些底层机制的猜想和调查时候,我们觉得这是一件很有意思的事情,于是在伙同赵锟进行过一系列的调查后,我俩发现了很多有趣的事情,于是导致了这篇文章的产生。这也是为什么本文的作者署名为我们两人的原因。

 

先要说明的一点是,赵锟和我本质上都是出身于Unix/Linux/C/C++的开发人员,对于Java,这并不是我们的长处,这篇文章本质上出于对Java的Selector的好奇,因为从表面上来看Selector似乎做到了一些让我们这些C/C++出身的人比较惊奇的事情。

 

下面让我来为你讲述一下这段故事。

 

二、  故事开始 : 让C++程序员写Java程序!

 

没有严重内存问题,大量丰富的SDK类库,超容易的跨平台,除了在性能上有些微辞,C++出身的程序员从来都不会觉得Java是一件很困难的事情。当然,对于长期习惯于使用操作系统API(系统调用System Call)的C/C++程序来说,面对Java中的比较“另类”地操作系统资源的方法可能会略感困惑,但万变不离其宗,只需要对面向对象的设计模式有一定的了解,用不了多长时间,Java的SDK类库也能玩得随心所欲。

 

在使用Java进行相关网络程序的的设计时,出身C/C++的人,首先想到的框架就是多路复用,想到多路复用,Unix/Linux下马上就能让从想到select, poll, epoll系统调用。于是,在看到Java的NIO中的Selector类时必然会倍感亲切。稍加查阅一下SDK手册以及相关例程,不一会儿,一个多路复用的框架便呈现出来,随手做个单元测试,没啥问题,一切和C/C++照旧。然后告诉兄弟们,框架搞定,以后咱们就在Windows上开发及单元测试,完成后到运行环境Unix上集成测试。心中并暗自念到,跨平台就好啊,开发活动都可以跨平台了。

 

然而,好景不长,随着代码越来越多,逻辑越来越复杂。好好的框架居然在Windows上单元测试运行开始出现异常,看着Java运行异常出错的函数栈,异常居然由Selector.open()抛出,错误信息居然是Unable to establish loopback connection。

 

“Selector.open()居然报loopback connection错误,凭什么?不应该啊?open的时候又没有什么loopback的socket连接,怎么会报这个错?”

 

长期使用C/C++的程序当然会对操作系统的调用非常熟悉,虽然Java的虚拟机搞的什么系统调用都不见了,但C/C++的程序员必然要比Java程序敏感许多。

 

三、  开始调查 : 怎么Java这么“傻”!

 

于是,C/C++的老鸟从SystemInternals上下载Process Explorer来查看一下究竟是什么个Loopback Connection。 果然,打开java运行进程,发现有一些自己连接自己的localhost的TCP/IP链接。于是另一个问题又出现了,

 

凭什么啊?为什么会有自己和自己的连接?我程序里没有自己连接自己啊,怎么可能会有这样的链接啊?而自己连接自己的端口号居然是些奇怪的端口。”

 

问题变得越来越蹊跷了。难道这都是Selector.open()在做怪?难道Selector.open()要创建一个自己连接自己的链接?写个程序看看:

 

import java.nio.channels.Selector;

import java.lang.RuntimeException;

import java.lang.Thread;

public class TestSelector {

    private static final int MAXSIZE=5;

    public static final void main( String argc[] ) {

        Selector [] sels = new Selector[ MAXSIZE];

 

            try{

                forint i = 0 ;i< MAXSIZE ;++i ) {

                    sels[i] = Selector.open();

                    //sels[i].close();

                }

                Thread.sleep(30000);

            }catch( Exception ex ){

                throw new RuntimeException( ex );

            }

    }

}

 

这个程序什么也没有,就是做5次Selector.open(),然后休息30秒,以便我使用Process Explorer工具来查看进程。程序编译没有问题,运行起来,在Process Explorer中看到下面的对话框:(居然有10个连接,从连接端口我们可以知道,互相连接, 如:第一个连第二个,第二个又连第一个

不由得赞叹我们的Java啊,先不说这是不是一件愚蠢的事。至少可以肯定的是,Java在消耗宝贵的系统资源方面,已经可以赶的上某些蠕虫病毒了。

 

如果不信,不妨把上面程序中的那个MAXSIZE的值改成65535试试,不一会你就会发现你的程序有这样的错误了:(在我的XP机器上大约运行到2000个Selector.open() 左右)

 

Exception in thread "main" java.lang.RuntimeException: java.io.IOException: Unable to establish loopback connection

        at Test.main(Test.java:18)

Caused by: java.io.IOException: Unable to establish loopback connection

        at sun.nio.ch.PipeImpl$Initializer.run(Unknown Source)

        at java.security.AccessController.doPrivileged(Native Method)

        at sun.nio.ch.PipeImpl.<init>(Unknown Source)

        at sun.nio.ch.SelectorProviderImpl.openPipe(Unknown Source)

        at java.nio.channels.Pipe.open(Unknown Source)

        at sun.nio.ch.WindowsSelectorImpl.<init>(Unknown Source)

        at sun.nio.ch.WindowsSelectorProvider.openSelector(Unknown Source)

        at java.nio.channels.Selector.open(Unknown Source)

        at Test.main(Test.java:15)

Caused by: java.net.SocketException: No buffer space available (maximum connections reached?): connect

        at sun.nio.ch.Net.connect(Native Method)

        at sun.nio.ch.SocketChannelImpl.connect(Unknown Source)

        at java.nio.channels.SocketChannel.open(Unknown Source)

        ... 9 more

 

 

四、  继续调查 : 如此跨平台

 

当然,没人像我们这么变态写出那么多的Selector.open(),但这正好可以让我们来明白Java背着大家在干什么事。上面的那些“愚蠢连接”是在Windows平台上,如果不出意外,Unix/Linux下应该也差不多吧。

 

于是我们把上面的程序放在Linux下跑了跑。使用netstat 命令,并没有看到自己和自己的Socket连接。貌似在Linux上使用了和Windows不一样的机制?!

 

如果在Linux上不建自己和自己的TCP连接的话,那么文件描述符和端口都会被省下来了,是不是也就是说我们调用65535个Selector.open()的话,应该不会出现异常了。

 

可惜,在实现运行过程序当中,还是一样报错:(大约在400个Selector.open()左右,还不如Windows)

 

Exception in thread "main" java.lang.RuntimeException: java.io.IOException: Too many open files

        at Test1.main(Test1.java:19)

Caused by: java.io.IOException: Too many open files

        at sun.nio.ch.IOUtil.initPipe(Native Method)

        at sun.nio.ch.EPollSelectorImpl.<init>(EPollSelectorImpl.java:49)

        at sun.nio.ch.EPollSelectorProvider.openSelector(EPollSelectorProvider.java:18)

        at java.nio.channels.Selector.open(Selector.java:209)

        at Test1.main(Test1.java:15)

 

我们发现,这个异常错误是“Too many open files”,于是我想到了使用lsof命令来查看一下打开的文件。

 

看到了有一些pipe文件,一共5对,10个(当然,管道从来都是成对的)。如下图所示。

 

 

 

可见,Selector.open()在Linux下不用TCP连接,而是用pipe管道。看来,这个pipe管道也是自己给自己的。所以,我们可以得出下面的结论:

 

1Windows下,Selector.open()会自己和自己建立两条TCP链接。不但消耗了两个TCP连接和端口,同时也消耗了文件描述符。

2Linux下,Selector.open()会自己和自己建两条管道。同样消耗了两个系统的文件描述符。

 

估计,在Windows下,Sun的JVM之所以选择TCP连接,而不是Pipe,要么是因为性能的问题,要么是因为资源的问题。可能,Windows下的管道的性能要慢于TCP链接,也有可能是Windows下的管道所消耗的资源会比TCP链接多。这些实现的细节还有待于更为深层次的挖掘。

 

但我们至少可以了解,原来Java的Selector在不同平台上的机制。


 

Java NIO类库Selector机制解析(下)

五、  迷惑不解 : 为什么要自己消耗资源?

 

令人不解的是为什么我们的Java的New I/O要设计成这个样子?如果说老的I/O不能多路复用,如下图所示,要开N多的线程去挨个侦听每一个Channel (文件描述符) ,如果这样做很费资源,且效率不高的话。那为什么在新的I/O机制依然需要自己连接自己,而且,还是重复连接,消耗双倍的资源?

 

通过WEB搜索引擎没有找到为什么。只看到N多的人在报BUG,但SUN却没有任何解释。

 

下面一个图展示了,老的IO和新IO的在网络编程方面的差别。看起来NIO的确很好很强大。但似乎比起C/C++来说,Java的这种实现会有一些不必要的开销。

 

 

 

六、  它山之石 : 从Apache的Mina框架了解Selector

 

上面的调查没过多长时间,正好同学赵锟的一个同事也在开发网络程序,这位仁兄使用了Apache的Mina框架。当我们把Mina框架的源码研读了一下后。发现在Mina中有这么一个机制:

 

1Mina框架会创建一个Work对象的线程。

2Work对象的线程的run()方法会从一个队列中拿出一堆Channel,然后使用Selector.select()方法来侦听是否有数据可以读/写。

3最关键的是,在select的时候,如果队列有新的Channel加入,那么,Selector.select()会被唤醒,然后重新select最新的Channel集合。

4要唤醒select方法,只需要调用Selector的wakeup()方法。

 

对于熟悉于系统调用的C/C++程序员来说,一个阻塞在select上的线程有以下三种方式可以被唤醒:

1  有数据可读/写,或出现异常。

2  阻塞时间到,即time out

3  收到一个non-block的信号。可由kill或pthread_kill发出。

所以,Selector.wakeup()要唤醒阻塞的select,那么也只能通过这三种方法,其中:

    1第二种方法可以排除,因为select一旦阻塞,应无法修改其time out时间。

2而第三种看来只能在Linux上实现,Windows上没有这种信号通知的机制。

 

所以,看来只有第一种方法了。再回想到为什么每个Selector.open(),在Windows会建立一对自己和自己的loopback的TCP连接;在Linux上会开一对pipe(pipe在Linux下一般都是成对打开),估计我们能够猜得出来——那就是如果想要唤醒select,只需要朝着自己的这个loopback连接发点数据过去,于是,就可以唤醒阻塞在select上的线程了。

 

七、  真相大白 : 可爱的Java你太不容易了

使用Linux下的strace命令,我们可以方便地证明这一点。参看下图。图中,请注意下面几点:

1  26654是主线程,之前我输出notify the select字符串是为了做一个标记,而不至于迷失在大量的strace log中。

2  26662是侦听线程,也就是select阻塞的线程。

3  图中选中的两行。26654的write正是wakeup()方法的系统调用,而紧接着的就是26662的epoll_wait的返回。

 

从上图可见,这和我们之前的猜想正好一样。可见,JDK的Selector自己和自己建的那些TCP连接或是pipe,正是用来实现Selector的notify和wakeup的功能的。

 

这两个方法完全是来模仿Linux中的的kill和pthread_kill给阻塞在select上的线程发信号的。但因为发信号这个东西并不是一个跨平台的标准(pthread_kill这个系统调用也不是所有Unix/Linux都支持的),而pipe是所有的Unix/Linux所支持的,但Windows又不支持,所以,Windows用了TCP连接来实现这个事。

 

关于Windows,我一直在想,Windows的防火墙的设置是不是会让Java的类似的程序执行异常呢?呵呵。如果不知道Java的SDK有这样的机制,谁知道会有多少个程序为此引起的问题度过多少个不眠之夜,尤其是Java程序员。

 

八、  后记

文章到这里是可以结束了,但关于Java NIO的Selector引出来的其它话题还有许多,比如关于GNU 的Java编译器又是如何,它是否会像Sun的Java解释器如此做傻事?我在这里先卖一个关子,关于GNU的Java编译器,我会在另外一篇文章中讲述,近期发布,敬请期待。

 

关于本文中所使用的实验平台如下:

·        Windows:Windows XP + SP2, Sun J2SE (build 1.7.0-ea-b23)

·        Linux:Ubuntu 7.10 + Linux Kernel 2.6.22-14-generic, J2SE (build 1.6.0_03-b05)

 

本文主要的调查工作由我的大学同学赵锟完成,我帮其验证调查成果及猜想。在此也向大家介绍我的大学同学赵锟,他也是一个技术高手,在软件开发方面,特别是Unix/Linux C/C++方面有着相当的功底,相信自此以后,会有很多文章会由我和他一同发布。

 

本篇文章由我成文。但其全部著作权和版权归赵锟和我共同所有。我们欢迎大家转载,但希望保持整篇文章的完整性,并请勿用于任何商业用途。谢谢。

java.io和java.nio性能简单对比

我从java1.3开始学习java,后来主要用1.4,再后来1.5和1.6中的很多新特性,都停留在“知道”的状态,比如nio,虽然据说可以提升性能,但并没有真正深入使用和测试过,工作操作文件的情况不多,所以关注也不多,即便用到,也还是习惯性的用java.io。今天看到的这篇文章,虽然测试手段非常简单,所得结论也难免有些片面 ,但依然说明,在顺序访问的时候,NIO的性能相对java.io有很大的提升。

也许应该update一下自己的知识了,否则就要OUT,或者早已经OUT了。 
下次操作文件或者写socket要用NIO了。

---
转自:http://links.techwebnewsletters.com/ctt?kn=28&m=34038811&r=MzI1Mjc3MDAzOAS2&b=0&j=NTc5NjM4MTAS1&mt=1&rt=0
以下为翻译的内容: 

最近我在工作中用到了java i/o相关功能。因为对java.io的了解更多(毕竟面世较早),所以一开始我使用的是java.io包下的类,后来为了测试一下是不是能够通过NIO提高文件操作性能,于是转向了java.nio。我得到的结论让我感到有些震惊,下面是对比测试的一些细节:

   1、在java.io的测试代码中,我使用RandomAccessFile直接向文件写数据,并搜索到特定的位置执行记录的插入、读取和删除。
   2、在java.nio的初步测试代码中,使用FileChannel对象。NIO之所以比java.io更加高效,是因为NIO面向的是data chunks,而java.io基本上是面向byte的。
   3、为了进一步挖掘NIO的能力,我又改用MappedByteBuffer执行测试,这个类是构建在操作系统的虚拟内存机制上的。根据java文档所说,这个类在性能方面是最好的。

为了进行测试,我写了一个模拟员工数据库的小程序,员工数据的结构如下:
   

view plaincopy to clipboardprint?

class Employee {   

        String last; // the key   

        String first;   

        int id;   

        int zip;   

        boolean employed;   

        String comments;   

    }  



员工数据写入文件,并将last name作为索引key,日后可以通过这个key从文件中加载该员工对应的数据。无论使用IO、NIO还是MappedByteBuffers,首先都需要打开一个RandomAccessFile。以下代码在用户的home目录下创建一个名为employee.ejb的文件,设置为可读可写,并初始化对应的Channel和MappedByteBuffer:
   

view plaincopy to clipboardprint?

String userHome = System.getProperty("user.home");   

    StringBuffer pathname = new StringBuffer(userHome);   

    pathname.append(File.separator);   

    pathname.append("employees.ejb");   

    java.io.RandomAccessFile journal =   

        new RandomAccessFile(pathname.toString(), "rw");   

    

    //下面这一句是为了NIO   

    java.nio.channels.FileChannel channel = journal.getChannel();   

      

    //下面这两句是为了使用MappedByteBuffer   

    journal.setLength(PAGE_SIZE);   

    MappedByteBuffer mbb = channel.map(FileChannel.MapMode.READ_WRITE, 0, journal.length() );  


使用channel.map进行映射后,当该文件被追加了新的数据时,之前的MappedByteBuffer是看不到这些数据的。因为我们想测试读和写,所以当文件中追加写入新的记录后,需要重新做映射才能使得MappedByteBuffer读取新数据。为了提高效率,降低重新映射的次数,每次空间不够的时候,我们将文件扩张特定的大小(比如说1K)以防止每次追加新记录都要重新映射。
下面是写入员工记录的对比测试:

使用java.io的代码:
view plaincopy to clipboardprint?

public boolean addRecord_IO(Employee emp) {   

        try {   

            byte[] last = emp.last.getBytes();   

            byte[] first = emp.first.getBytes();   

            byte[] comments = emp.comments.getBytes();   

              

            // Just hard-code the sizes for perfomance   

            int size = 0;   

            size += emp.last.length();   

            size += 4// strlen - Integer   

            size += emp.first.length();   

            size += 4// strlen - Integer   

            size += 4// emp.id - Integer   

            size += 4// emp.zip - Integer   

            size += 1// emp.employed - byte   

            size += emp.comments.length();   

            size += 4// strlen - Integer   

            long offset = getStorageLocation(size);   

            //   

            // Store the record by key and save the offset   

            //   

            if ( offset == -1 ) {   

                // We need to add to the end of the journal. Seek there   

                // now only if we're not already there   

                long currentPos = journal.getFilePointer();   

                long jounralLen = journal.length();   

                if ( jounralLen != currentPos )   

                    journal.seek(jounralLen);   

                      

                offset = jounralLen;   

            }else {   

                // Seek to the returned insertion point   

                journal.seek(offset);   

            }   

            // Fist write the header   

            journal.writeByte(1);   

            journal.writeInt(size);   

            // Next write the data   

            journal.writeInt(last.length);   

            journal.write(last);   

            journal.writeInt(first.length);   

            journal.write(first);   

            journal.writeInt(emp.id);   

            journal.writeInt(emp.zip);   

            if ( emp.employed )   

                journal.writeByte(1);   

            else  

                journal.writeByte(0);   

            journal.writeInt(comments.length);   

            journal.write(comments);   

            // Next, see if we need to append an empty record if we inserted   

            // this new record at an empty location   

            if ( newEmptyRecordSize != -1 ) {   

                // Simply write a header   

                journal.writeByte(0); //inactive record   

                journal.writeLong(newEmptyRecordSize);   

            }   

            employeeIdx.put(emp.last, offset);   

            return true;   

        }   

        catch ( Exception e ) {   

            e.printStackTrace();   

        }   

        return false;   

    }  
 
使用java.nio的代码:
   

view plaincopy to clipboardprint?

public boolean addRecord_NIO(Employee emp) {   

        try {   

            data.clear();   

            byte[] last = emp.last.getBytes();   

            byte[] first = emp.first.getBytes();   

            byte[] comments = emp.comments.getBytes();   

            data.putInt(last.length);   

            data.put(last);   

            data.putInt(first.length);   

            data.put(first);   

            data.putInt(emp.id);   

            data.putInt(emp.zip);   

            byte employed = 0;   

            if ( emp.employed )   

                employed = 1;   

            data.put(employed);   

            data.putInt(comments.length);   

            data.put(comments);   

            data.flip();   

            int dataLen = data.limit();   

            header.clear();   

            header.put((byte)1); // 1=active record   

            header.putInt(dataLen);   

            header.flip();   

            long headerLen = header.limit();   

            int length = (int)(headerLen + dataLen);   

            long offset = getStorageLocation((int)dataLen);   

            //   

            // Store the record by key and save the offset   

            //   

            if ( offset == -1 ) {   

                // We need to add to the end of the journal. Seek there   

                // now only if we're not already there   

                long currentPos = channel.position();   

                long jounralLen = channel.size();   

                if ( jounralLen != currentPos )   

                    channel.position(jounralLen);   

                offset = jounralLen;   

            }   

            else {   

                // Seek to the returned insertion point   

                channel.position(offset);   

            }   

            // Fist write the header   

            long written = channel.write(srcs);   

            // Next, see if we need to append an empty record if we inserted   

            // this new record at an empty location   

            if ( newEmptyRecordSize != -1 ) {   

                // Simply write a header   

                data.clear();   

                data.put((byte)0);   

                data.putInt(newEmptyRecordSize);   

                data.flip();   

                channel.write(data);   

            }   

            employeeIdx.put(emp.last, offset);   

            return true;   

        }   

        catch ( Exception e ) {   

            e.printStackTrace();   

        }   

        return false;   

    }  
 
使用MappedByteBuffer的代码如下:
   

view plaincopy to clipboardprint?

public boolean addRecord_MBB(Employee emp) {   

        try {   

            byte[] last = emp.last.getBytes();   

            byte[] first = emp.first.getBytes();   

            byte[] comments = emp.comments.getBytes();   

            int datalen = last.length + first.length + comments.length + 12 + 9;   

            int headerlen = 5;   

            int length = headerlen + datalen;   

            //   

            // Store the record by key and save the offset   

            //   

            long offset = getStorageLocation(datalen);   

            if ( offset == -1 ) {   

                // We need to add to the end of the journal. Seek there   

                // now only if we're not already there   

                long currentPos = mbb.position();   

                long journalLen = channel.size();   

                if ( (currentPos+length) >= journalLen ) {   

                    //log("GROWING FILE BY ANOTHER PAGE");   

                    mbb.force();   

                    journal.setLength(journalLen + PAGE_SIZE);   

                    channel = journal.getChannel();   

                    journalLen = channel.size();   

                    mbb = channel.map(FileChannel.MapMode.READ_WRITE, 0, journalLen);   

                    currentPos = mbb.position();   

                }   

                if ( currentEnd != currentPos )   

                    mbb.position(currentEnd);   

                offset = currentEnd;//journalLen;   

            }   

            else {   

                // Seek to the returned insertion point   

                mbb.position((int)offset);   

            }   

            // write header   

            mbb.put((byte)1); // 1=active record   

            mbb.putInt(datalen);   

            // write data   

            mbb.putInt(last.length);   

            mbb.put(last);   

            mbb.putInt(first.length);   

            mbb.put(first);   

            mbb.putInt(emp.id);   

            mbb.putInt(emp.zip);   

            byte employed = 0;   

            if ( emp.employed )   

                employed = 1;   

            mbb.put(employed);   

            mbb.putInt(comments.length);   

            mbb.put(comments);   

            currentEnd += length;   

            // Next, see if we need to append an empty record if we inserted   

            // this new record at an empty location   

            if ( newEmptyRecordSize != -1 ) {   

                // Simply write a header   

                mbb.put((byte)0);   

                mbb.putInt(newEmptyRecordSize);   

                currentEnd += 5;   

            }   

            employeeIdx.put(emp.last, offset);   

            return true;   

        }   

        catch ( Exception e ) {   

            e.printStackTrace();   

        }   

        return false;   

    }  


 
接下来,调用每种方法插入100,000条记录, 耗时对比如下:
    * With java.io: ~10,000 milliseconds
    * With java.nio: ~2,000 milliseconds
    * With MappedByteBuffer: ~970 milliseconds
 使用NIO的性能改善效果非常明显,使用MappedByteBuffer的性能,更是让人吃惊。

使用三种方式读取数据的性能对比如下:
    * With java.io: ~6,900 milliseconds
    * With java.nio: ~1,400 milliseconds
    * With MappedByteBuffer: ~355 milliseconds
和写入的时候情况差不多,NIO有很明显的性能提升,而MappedByteBuffer则有惊人的高效率。从java.io迁移到nio并使用MappedByteBuffer,通常可以获得10倍以上的性能提升。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值