java的nio的使用示例分享

Java NIO(New Input/Output)——新的输入/输出API包——是2002年引入到J2SE 1.4里的。Java NIO的目标是提高Java平台上的I/O密集型任务的性能。过了十年,很多Java开发者还是不知道怎么充分利用NIO,更少的人知道在Java SE 7里引入了更新的输入/输出 API(NIO.2)。NIO和NIO.2对于Java平台最大的贡献是提高了Java应用开发中的一个核心组件的性能:输入/输出处理。不过这两个包都不是很好用,并且它们也不是适用于所有的场景。如果能够正确地使用的话,Java NIO和NIO.2可以大大减少一些常用I/O操作所花的时间。这就是NIO和NIO.2所具有的超能力,我会在这篇文章里向你展示5种使用它们的简单方式。

变更通知(因为每个事件都需要一个监听者)
选择器和异步IO:通过选择器来提高多路复用
通道——承诺与现实
内存映射——好钢用在刀刃上
字符编码和搜索
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,主机的虚拟上下文,大容量存储的特性甚至和数据也是相关的。因此,性能衡量的工作是比较难做的。尤其是当你的系统存在一个可移动的部署环境的时候,你需要特别注意。

了解了上面的内容后,我们没有后顾之忧了,现在就来体验一下NIO和NIO.2的5个重要的功能。

1. 变更通知(因为每个事件都需要一个监听者)

对NIO和NIO.2有兴趣的开发者的共同关注点在于Java应用的性能。根据我的经验,NIO.2里的文件变更通知者(file change notifier)是新输入/输出API里最让人感兴趣(被低估了)的特性。

很多企业级应用需要在下面的情况时做一些特殊的处理:

当一个文件上传到一个FTP文件夹里时
当一个配置里的定义被修改时
当一个草稿文档被上传时
其他的文件系统事件出现时
这些都是变更通知或者变更响应的例子。在Java(以及其他语言)的早期版本里,轮询(polling)是检测这些变更事件的最好方式。轮询是一种特殊的无限循环:检查文件系统或者其他对象,并且和之前的状态对比,如果没有变化,在大概几百个毫秒或者10秒的间隔后,继续检查。就这一直无限循环下去。

NIO.2提供了一个更好地方式来进行变更检测。列表1是一个简单的示例。

列表1. NIO.2里的变更通知机制

代码如下:

import java.nio.file.attribute.*;
importjava.io.*;
importjava.util.*;
importjava.nio.file.Path;
importjava.nio.file.Paths;
importjava.nio.file.StandardWatchEventKinds;
importjava.nio.file.WatchEvent;
importjava.nio.file.WatchKey;
importjava.nio.file.WatchService;
importjava.util.List;

publicclassWatcher{
publicstaticvoidmain(String[]args){
Paththis_dir=Paths.get(".");
System.out.println("Nowwatchingthecurrentdirectory...");

try{
WatchServicewatcher=this_dir.getFileSystem().newWatchService();
this_dir.register(watcher,StandardWatchEventKinds.ENTRY_CREATE);

WatchKeywatckKey=watcher.take();

List<WatchEvent<<64;>>events=watckKey.pollEvents();
for(WatchEventevent:events){
System.out.println("Someonejustcreatedthefile'"+event.context().toString()+"'.");

}

}catch(Exceptione){
System.out.println("Error:"+e.toString());
}
}
}

编译这段代码,然后在命令行里执行。在相同的目录下,创建一个新的文件,例如运行touchexample或者copyWatcher.classexample命令。你会看到下面的变更通知消息:

Someonejustcreatethefiel‘example1′.

这个简单的示例展示了怎么开始使用JavaNIO的功能。同时,它也介绍了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,这里是修改了GregTravis在2003年创建的一个小程序(参考资源列表)。Unix和类Unix系统很早就已经实现高效的选择器,它是Java网络高性能编程模型的一个很好的参考模型。

列表2.NIO选择器

代码如下:

importjava.io.*;
importjava.net.*;
importjava.nio.*;
importjava.nio.channels.*;
importjava.util.*;

publicclassMultiPortEcho
{
privateintports[];
privateByteBufferechoBuffer=ByteBuffer.allocate(1024);

publicMultiPortEcho(intports[])throwsIOException{
this.ports=ports;

configure_selector();
}

privatevoidconfigure_selector()throwsIOException{
//Createanewselector
Selectorselector=Selector.open();

//Openalisteneroneachport,andregistereachone
//withtheselector
for(inti=0;i<ports.length;++i){
ServerSocketChannelssc=ServerSocketChannel.open();
ssc.configureBlocking(false);
ServerSocketss=ssc.socket();
InetSocketAddressaddress=newInetSocketAddress(ports[i]);
ss.bind(address);

SelectionKeykey=ssc.register(selector,SelectionKey.OP_ACCEPT);

System.out.println("Goingtolistenon"+ports[i]);
}

while(true){
intnum=selector.select();

SetselectedKeys=selector.selectedKeys();
Iteratorit=selectedKeys.iterator();

while(it.hasNext()){
SelectionKeykey=(SelectionKey)it.next();

if((key.readyOps()&SelectionKey.OP_ACCEPT)
==SelectionKey.OP_ACCEPT){
//Acceptthenewconnection
ServerSocketChannelssc=(ServerSocketChannel)key.channel();
SocketChannelsc=ssc.accept();
sc.configureBlocking(false);

//Addthenewconnectiontotheselector
SelectionKeynewKey=sc.register(selector,SelectionKey.OP_READ);
it.remove();

System.out.println("Gotconnectionfrom"+sc);
}elseif((key.readyOps()&SelectionKey.OP_READ)
==SelectionKey.OP_READ){
//Readthedata
SocketChannelsc=(SocketChannel)key.channel();

//Echodata
intbytesEchoed=0;
while(true){
echoBuffer.clear();

intnumber_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();
}

}
}
}

staticpublicvoidmain(Stringargs[])throwsException{
if(args.length<=0){
System.err.println("Usage:javaMultiPortEchoport[portport...]");
System.exit(1);
}

intports[]=newint[args.length];

for(inti=0;i<args.length;++i){
ports[i]=Integer.parseInt(args[i]);
}

newMultiPortEcho(ports);
}
}

编译这段代码,然后通过类似于javaMultiPortEcho80058006这样的命令来启动它。一旦这个程序运行成功,启动一个简单的telnet或者其他的终端模拟器来连接8005和8006接口。你会看到这个程序会回显它接收到的所有字符——并且它是通过一个Java线程来实现的。

3.通道:承诺与现实

在NIO里,一个通道(channel)可以表示任何可以读写的对象。它的作用是为文件和套接口提供抽象。NIO通道支持一系列一致的方法,这样就使得编码的时候不需要去特别关心不同的对象,无论它是标准输出,网络连接还是正在使用的通道。通道的这个特性是继承自Java基本I/O中的流(stream)。流(stream)提供了阻塞式的IO;通道支持异步I/O。

NIO经常会因为它的性能高而被推荐,不过更准确地是因为它的响应快速。在有些场景下NIO会比基本的JavaI/O的性能要差。例如,对于一个小文件的简单的顺序读写,简单通过流来实现的性能可能比对应的面向事件的基于通道的编码实现的快两到三倍。同时,非多路复用(non-multiplex)的通道——也就是每个线程一个单独的通道——要比多个通道把各自的选择器注册在同一个线程里要慢多了。

下面你在考虑是使用流还是通道的时候,试着问自己下面几个问题:

你需要读写多少个I/O对象?
不同的I/O对象直接是否有有顺序,还是他们都需要同时发生的?
你的I/O对象是需要持续一小段时间还是在你的进程的整个声明周期都存在?
你的I/O是适合在单个线程里处理还是在几个不同的线程里?
网络通信和本地I/O是看起来一样,还是各自有着不同的模式?
这样的分析是决定使用流还是通道的一个最佳实践。记住:NIO和NIO.2不是基本I/O的替代,而它的一个补充。

4.内存映射——好钢用在刀刃上

NIO里对性能提升最显著的是内存映射(memorymapping)。内存映射是一个系统层面的服务,它把程序里用到的文件的一段当作内存来处理。

内存映射存在很多潜在的影响,比我这里提供的要多。在一个更高的层次上,它能够使得文件访问的I/O的性能达到内存访问的速度。内存访问的速度往往比文件访问的速度快几个数量级。列表3是一个NIO内存映射的一个简单示例。

列表3.NIO里的内存映射

代码如下:

importjava.io.RandomAccessFile;
importjava.nio.MappedByteBuffer;
importjava.nio.channels.FileChannel;

publicclassmem_map_example{
privatestaticintmem_map_size=20*1024*1024;
privatestaticStringfn="example_memory_mapped_file.txt";

publicstaticvoidmain(String[]args)throwsException{
RandomAccessFilememoryMappedFile=newRandomAccessFile(fn,"rw");

//Mappingafileintomemory
MappedByteBufferout=memoryMappedFile.getChannel().map(FileChannel.MapMode.READ_WRITE,0,mem_map_size);

//WritingintoMemoryMappedFile
for(inti=0;i<mem_map_size;i++){
out.put((byte)'A');
}
System.out.println("File'"+fn+"'isnow"+Integer.toString(mem_map_size)+"bytesfull.");

//Readfrommemory-mappedfile.
for(inti=0;i<30;i++){
System.out.print((char)out.get(i));
}
System.out.println("\nReadingfrommemory-mappedfile'"+fn+"'iscomplete.");
}
}

在列表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里的字符

代码如下:

Stringsome_string="ThisisastringthatJavanativelystoresasUnicode.";
Charsetlatin1_charset=Charset.forName("ISO-8859-1");
CharsetEncodelatin1_encoder=charset.newEncoder();
ByteBufferlatin1_bbuf=latin1_encoder.encode(CharBuffer.wrap(some_string));

注意Charset和通道被设计成能够放在一起进行使用,这样就能够使得程序在内存映射、异步I/O以及编码转换进行协作的时候,能够正常运行。

总结:当然还有更多需要去了解

这篇文章的目的是为了让Java开发者能够熟悉NIO和NIO.2里的一些最主要(也是最有用)的功能。你可以通过这些示例建立起来的一些基础来理解NIO的一些其他方法;例如,你所学习的关于通道的知识能够帮助你去理解NIO的Path里对于文件系统里的符号链接的处理。你也可以参考一下我后面给出的资源列表,里面给出了一些深入学习Java新I/OAPI的文档。

(0)

相关推荐

  • Java NIO工作原理的全面分析

    ◆  输入/输出:概念性描述I/O 简介I/O ? 或者输入/输出 ? 指的是计算机与外部世界或者一个程序与计算机的其余部分的之间的接口.它对于任何计算机系统都非常关键,因而所有 I/O 的主体实际上是内置在操作系统中的.单独的程序一般是让系统为它们完成大部分的工作.在 Java 编程中,直到最近一直使用 流 的方式完成 I/O.所有 I/O 都被视为单个的字节的移动,通过一个称为 Stream 的对象一次移动一个字节.流 I/O 用于与外部世界接触.它也在内部使用,用于将对象转换为字节,然后再

  • java使用nio2拷贝文件的示例

    这个程序只是为了更方便的进行拷贝文件(夹)而创造.1.可以不用新建文件夹,就像windows的复制粘贴一样简单.2.有简单的出错重连机制3.不需要重复拷贝,差异化复制文件.4.拷贝文件夹的时候可以不用复制全路径,只关注需要拷贝的文件夹.5.程序做了简单的必要检查,效率也不算低.6.使用的是7的nio2的新API. 复制代码 代码如下: import java.io.IOException;import java.nio.file.FileVisitResult;import java.nio.f

  • Java 高并发八:NIO和AIO详解

    IO感觉上和多线程并没有多大关系,但是NIO改变了线程在应用层面使用的方式,也解决了一些实际的困难.而AIO是异步IO和前面的系列也有点关系.在此,为了学习和记录,也写一篇文章来介绍NIO和AIO. 1. 什么是NIO NIO是New I/O的简称,与旧式的基于流的I/O方法相对,从名字看,它表示新的一套Java I/O标 准.它是在Java 1.4中被纳入到JDK中的,并具有以下特性: NIO是基于块(Block)的,它以块为基本单位处理数据 (硬盘上存储的单位也是按Block来存储,这样性能

  • Java NIO和IO的区别

    下表总结了Java NIO和IO之间的主要差别,我会更详细地描述表中每部分的差异. 复制代码 代码如下: IO                NIO面向流            面向缓冲阻塞IO            非阻塞IO无                选择器 面向流与面向缓冲 Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的. Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方.此外,它不能前后移动流中的数

  • Java NIO:浅析IO模型_动力节点Java学院整理

    也许很多朋友在学习NIO的时候都会感觉有点吃力,对里面的很多概念都感觉不是那么明朗.在进入Java NIO编程之前,我们今天先来讨论一些比较基础的知识:I/O模型.下面本文先从同步和异步的概念 说起,然后接着阐述了阻塞和非阻塞的区别,接着介绍了阻塞IO和非阻塞IO的区别,然后介绍了同步IO和异步IO的区别,接下来介绍了5种IO模型,最后介绍了两种和高性能IO设计相关的设计模式(Reactor和Proactor). 以下是本文的目录大纲: 一.什么是同步?什么是异步? 二.什么是阻塞?什么是非阻塞

  • java NIO 详解

    Java NIO提供了与标准IO不同的IO工作方式: Channels and Buffers(通道和缓冲区):标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中. Asynchronous IO(异步IO):Java NIO可以让你异步的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情.当数据被写入到缓冲区时,线程可以继续处理它.从缓冲区写入通道也类似. S

  • java的nio的使用示例分享

    Java NIO(New Input/Output)--新的输入/输出API包--是2002年引入到J2SE 1.4里的.Java NIO的目标是提高Java平台上的I/O密集型任务的性能.过了十年,很多Java开发者还是不知道怎么充分利用NIO,更少的人知道在Java SE 7里引入了更新的输入/输出 API(NIO.2).NIO和NIO.2对于Java平台最大的贡献是提高了Java应用开发中的一个核心组件的性能:输入/输出处理.不过这两个包都不是很好用,并且它们也不是适用于所有的场景.如果能

  • java的NIO管道用法代码分享

    Java的NIO中的管道,就类似于实际中的管道,有两端,一段作为输入,一段作为输出.也就是说,在创建了一个管道后,既可以对管道进行写,也可以对管道进行读,不过这两种操作要分别在两端进行.有点类似于队列的方式. 这里是Pipe原理的图示: 创建管道 通过Pipe.open()方法打开管道.例如: Pipe pipe = Pipe.open(); 向管道写数据 要向管道写数据,需要访问sink通道.像这样: Pipe.SinkChannel sinkChannel = pipe.sink(); 通过

  • java使用xpath解析xml示例分享

    XPath即为XML路径语言(XML Path Language),它是一种用来确定XML文档中某部分位置的语言.XPath基于XML的树状结构,提供在数据结构树中找寻节点的能力.起初 XPath 的提出的初衷是将其作为一个通用的.介于XPointer与XSL间的语法模型.但是 XPath 很快的被开发者采用来当作小型查询语言. XPathTest.java 复制代码 代码如下: package com.hongyuan.test; import java.io.File;import java

  • java模拟hibernate一级缓存示例分享

    纯Java代码模拟Hibernate一级缓存原理,简单易懂. 复制代码 代码如下: import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map; public class LevelOneCache { //这个对象就是用来模拟hibernate一级缓存的 private static Map<Integer, Student> stus=new HashMap&l

  • java判断回文数示例分享

    判断一个数是不是回文数示例,回文数就是原数与其倒置后的数相等,如:123321,到之后仍为123321,即为回文数 题目:一个5位数,判断它是不是回文数.即12321是回文数,个位与万位相同,十位与千位相同. /** * 判断一个数是不是回文数,回文数就是原数与其倒置后的数相等 * 如:123321,到之后仍为123321,即为回文数 * @author lvpeiqiang */ public class HuiWenShu { public boolean isHuiWenShu(int n

  • java打印当前方法名示例分享

    在C与C++中可以这样打印当前函数名: 复制代码 代码如下: printf("%s",__func__); 但在Java没有此说法,一切即对象,得从某个对象中去获取,可分为两种方式: 第一种:通过Thread类来获取. 复制代码 代码如下: System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());System.out.println(Thread.currentThread().getS

  • java集合求和最大值最小值示例分享

    复制代码 代码如下: package com.happyelements.athene.game.util; import static com.google.common.base.Preconditions.checkNotNull; import java.util.Collection; import com.google.common.collect.Lists; /** * Math工具类 *  * @version 1.0 * @since 1.0 */public class M

  • java使用des加密解密示例分享

    复制代码 代码如下: import java.security.Key;import java.security.SecureRandom;import java.security.spec.AlgorithmParameterSpec; import javax.crypto.Cipher;import javax.crypto.SecretKeyFactory;import javax.crypto.spec.DESKeySpec;import javax.crypto.spec.IvPar

  • java使用jdbc操作数据库示例分享

    package dao; import java.sql.*; public class BaseDao { //oracle// private  static final String Dirver="oracle.jdbc.driver.OracleDriver";// private  static final String URL="jdbc:oracle:thin:@localhost:1521:XE";// private  static final

  • java实现的小时钟示例分享

    复制代码 代码如下: //package com.clock; import java.awt.BasicStroke;import java.awt.Color;import java.awt.Font;import java.awt.Graphics;import java.awt.Graphics2D;import java.awt.Insets;import java.awt.event.ActionEvent;import java.awt.event.ActionListener;i

随机推荐