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包的简单示例。

NIO和NIO.2对于Java平台最大的贡献是提高了Java应用开发中的一个核心组件的性能:输入/输出处理。不过这两个包都不是很好用,并且它们也不是适用于所有的场景。如果能够正确地使用的话,Java NIO和NIO.2可以大大减少一些常用I/O操作所花的时间。这就是NIO和NIO.2所具有的超能力,我会在这篇文章里向你展示5种使用它们的简单方式。

  1. 变更通知(因为每个事件都需要一个监听者)
  2. 选择器和异步IO:通过选择器来提高多路复用
  3. 通道——承诺与现实
  4. 内存映射——好钢用在刀刃上
  5. 字符编码和搜索

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里的变更通知机制

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)的通道——也就是每个线程一个单独的通道——要比多个通道把各自的选择器注册在同一个线程里要慢多了。

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

  • 你需要读写多少个I/O对象?
  • 不同的I/O对象直接是否有有顺序,还是他们都需要同时发生的?
  • 你的I/O对象是需要持续一小段时间还是在你的进程的整个声明周期都存在?
  • 你的I/O是适合在单个线程里处理还是在几个不同的线程里?
  • 网络通信和本地I/O是看起来一样,还是各自有着不同的模式?

这样的分析是决定使用流还是通道的一个最佳实践。记住: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的文档。

关于作者

Cameron Laird 在Java被称作Java之前就开始写Java代码,并且从那时候开始就一直为JavaWorld做贡献。你可以通过他的Twitter@Phaseit来关注他的编码和写作。

英文原文:Java World,编译:ImportNew - 朱伟杰

5种调优Java NIO和NIO.2的方式的更多相关文章

  1. 操作系统实现线程的几种模式 和 java创建线程的3个方式

    操作系统实现线程的几种模式 和 java创建线程的3个方式  这是两个概念 在操作系统中,线程可以实现在用户模式下,也可以实现在内核模式下,也可以两者结合实现. 1.实现线程的三种方式: (1)继承t ...

  2. 调优Java virtual machine常见问题汇总整理

    数据类型 Java虚拟机中,数据类型可以分为两类:基本类型和引用类型.基本类型的变量保存原始值,即:他代表的值就是数值本身:而引用类型的变量保存引用值.“引用值”代表了某个对象的引用,而不是对象本身, ...

  3. JVM调优-Java垃圾回收之分代回收

    为什么要进行分代回收? JVM使用分代回收测试,是因为:不同的对象,生命周期是不一样的.因此不同生命周期的对象采用不同的收集方式. 可以提高垃圾回收的效率. Java程序运行过程中,会产生大量的对象, ...

  4. HBase的几种调优(GC策略,flush,compact,split)

    一:GC的调优 1.jvm的内存 新生代:存活时间较短,一般存储刚生成的一些对象 老年代:存活时间较长,主要存储在应用程序中生命周期较长的对象 永久代:一般存储meta和class的信息 2.GC策略 ...

  5. 082 HBase的几种调优(GC策略,flush,compact,split)

    一:GC的调优 1.jvm的内存 新生代:存活时间较短,一般存储刚生成的一些对象 老年代:存活时间较长,主要存储在应用程序中生命周期较长的对象 永久代:一般存储meta和class的信息 2.GC策略 ...

  6. 性能调优 -- Java编程中的性能优化

    String作为我们使用最频繁的一种对象类型,其性能问题是最容易被忽略的.作为Java中重要的数据类型,是内存中占据空间比较大的一个对象.如何高效地使用字符串,可以帮助我们提升系统的整体性能. 现在, ...

  7. JVM调优-Java中的对象

    Java对象的大小 基本数据的类型的大小是固定的,这里不做详细说明.对于非基本类型的Java对象,其大小就值得商榷. 在Java中,一个空Object对象的大小是8byte,这个大小只是保存堆中一个没 ...

  8. Java内存与垃圾回收调优

     Java(JVM)内存模型 正如你从上面的图片看到的,JVM内存被分成多个独立的部分.广泛地说,JVM堆内存被分为两部分——年轻代(Young Generation)和老年代(Old Generat ...

  9. 成为Java GC专家(5)—Java性能调优原则

    并不是每个程序都需要调优.如果一个程序性能表现和预期一样,你不必付出额外的精力去提高它的性能.然而,在程序调试完成之后,很难马上就满足它的性能需求,于是就有了调优这项工作.无论哪种编程语言,对应用程序 ...

随机推荐

  1. [BZOJ4811][YNOI2017]由乃的OJ(树链剖分+线段树)

    起床困难综合症那题,只要从高往低贪心,每次暴力跑一边看这一位输入0和1分别得到什么结果即可. 放到序列上且带修改,只要对每位维护一个线段树,每个节点分别记录0和1从左往右和从右往左走完这段区间后变成的 ...

  2. hdu 1026 bfs+记录路径

    题意:从0,0点出发到n-1,m-1点,路上的数字代表要在这个点额外待多少秒,求最短的路 递归输出路径即可 #include<cstdio> #include<iostream> ...

  3. Linux知识(7)----远程登录 和远程拷贝

    一.远程登录 1.安装客户端 可以使用ssh(Secure Shell(缩写为SSH))来进行远程的登录.安装ssh的命令为: sudo apt-get install openssh-server ...

  4. 推荐一个简洁优雅的博客系统,farbox

    这是我用farbox搞的一个博客:http://www.jsnull.com/ 特点: 1.无数据库,数据存在dropbox里,需要自己注册一个dropbox帐号 2.静态文本文件即是文章,可以在任何 ...

  5. mysql的性能监控指标(转载)

    这里列出了一些如何监视你安装的mysql性能的一些ideas.监视总是一个持续的过程.你需要知道哪种模式对你的数据库是好的,什么是问题的表象,甚至是危险的情况.一下列出了用来去监视你的系统的主要参数: ...

  6. The YubiKey -- COMPARISON OF VERSIONS

    COMPARISON OF YUBIKEY VERSIONS   BASICSTANDARD & NANO BASICEDGE & EDGE-N PREMIUMNEO & NE ...

  7. WINDOWS 的 MKLINK : 硬链接,符号链接 : 文件符号链接, 目录符号链接 : 目录联接

    玩转WIN7的MKLINK 引言: 换了新电脑,终于再次使用上啦WIN7 ,经过一个周每天重装N次系统,... ... ... ... 在xp系统下,junction命令要用微软开发的小程序 junc ...

  8. svn(subversion)版本控制系统学习与理解

    定义:Apache Subversion(简称SVN,svn),一个开放源代码的版本控制系统,相较于RCS.CVS,它采用了分支管理系统,它的设计目标就是取代CVS. 从这段话,我们可以得到四点信息: ...

  9. Ubuntu 查看本机的ip

    打开终端中执行:ifconfig -a命令即可,如下图所示白色背景信息即是. 说明: enp0s3 表示第一块网卡, 其中 HWaddr 表示网卡的物理地址,可以看到目前这个网卡的物理地址(MAC地址 ...

  10. word排版汇总

    1.iText是著名的开放源码的站点sourceforge一个项目,是用于生成PDF文档的一个java类库.通过iText不仅可以生成PDF或rtf 的文档,而且可以将XML.Html文件转化为PDF ...