NIO.2另一个新特性是异步能力,这种能力对套接字和文件I/O都适用。异步I/O其实只是一种在读写操作结束前允许进行其他操作的I/O处理。实际上,就是可以充分利用最新的硬件和软件特性,比如多核CPU及操作系统对套接字和文件处理的支持。对于任何想在服务器端和系统级编程领域占有一席之地的编程语言来说,异步I/O都是必不可少的特性。我们相信,Java在服务器端编程语言中所取得的重要地位会因为该特性得以延续。
举个简单的例子,想象一下你要把100GB的数据写入文件系统或网络套接字中。如果你用的是老版本的Java,在同时把数据写入文件或套接字的多个区域时,必须亲自动手用java.util.concurrent
写多线程代码。当然,同时进行多路读取也不容易。除非你写的代码十分巧妙,否则在使用I/O时也会阻塞主线程,这意味着在你完成漫长的I/O操作之前,除了等待,还是等待。
提示 如果你还没接触过NIO通道,也许可以趁此机会充实下你的知识结构。不过这个领域的新内容很少,但在继续本节内容之前,我们建议你去看看Ron Hitchens写的Java NIO(O/'Reilly,2002)一书,你会从中获益匪浅。
Java 7中有三个新的异步通道:
AsynchronousFileChannel
——用于文件I/O;AsynchronousSocketChannel
——用于套接字I/O,支持超时;AsynchronousServerSocketChannel
——用于套接字接受异步连接。
使用新的异步I/O API时,主要有两种形式,将来式和回调式。有趣的是,这些异步API用到了第4章讨论的一些现代并发技术,所以这真是让你先睹为快了。
我们会从异步文件访问的将来式开始。希望你已经用过这种并发技术,但如果没有用过,也不用担心,本节将会讲解得非常详细,即便是刚接触这个话题的新手也能看明白。
2.5.1 将来式
NIO.2 API的设计人员用将来(future)式这个术语来表明使用java.util.concurrent.Future
接口。当你希望由主控线程发起I/O操作并轮询等待结果时,一般都会用将来式异步处理。
将来式用现有的java.util.concurrent
技术声明一个Future
,用来保存异步操作的处理结果。这很关键,因为这意味着当前线程不会因为比较慢的I/O操作而停滞。相反,有一个单独的线程发起I/O操作,并在操作完成时返回结果。与此同时,主线程可以继续执行其他需要完成的任务。在其他任务结束后,如果I/O操作还没有完成,主线程会一直等待。图2-3演示了一个用将来式读取大型文件的过程。(代码清单2-8是相应的实现代码。)
图2-3 将来式异步读取
通常会用Future get
方法(带或不带超时参数)在异步I/O操作完成时获取其结果。假设你要从硬盘上的文件里读取100 000个字节,在旧版的Java中,你需要等待数据读取完成(除非你实现了一个线程池,而且工作线程使用java.util.concurrent
技术,这可不是件轻松的事儿)。而在Java 7中,主线程可以在读取数据的同时继续完成其他工作,如下面的代码所示。
代码清单2-8 异步I/O——将来式
try{ Path file = Paths.get(/"/usr/karianna/foobar.txt/"); AsynchronousFileChannel channel = AsynchronousFileChannel.open(file); //① 异步打开文件 /**②读取100 000字节*/ ByteBuffer buffer = ByteBuffer.allocate(100_000); Future<Integer> result = channel.read(buffer,0); while(!result.isDone) { ProfitCalculator.calculateTax;//③干点儿别的事情 } Integer bytesRead = result.get; //④获取结果 System.out.println(/"Bytes read [/" + bytesRead + /"]/");}catch (IOException | ExecutionException | InterruptedException e){ System.out.println(e.getMessage);}
上面的代码一开始先用后台进程中打开一个AsynchronousFileChannel
读/写foobar.txt①。接下来的这一步是为了让I/O处理能跟发起它的线程同步进行。因为采用AsynchronousFileChannel
,并用Future
保存读取结果,所以会自动采用并发的I/O处理②。在读取数据时,主线程可以继续执行任务(比如算一下要交多少税)③。最后,当任务完成时,你可以检查数据读取结果④。
一定要注意,我们在这里用isDone
手工判定result是否结束。通常情况下,result或结束(主线程会继续执行),或等待后台I/O完成。
你可能会好奇这究竟是怎么实现的。长话短说,API/JVM为执行这个任务创建了线程池和通道组。另外,你也可以自己提供和配置一个。解释其中的细节颇费口舌,并且官方文档都解释过了,所以我们只是直接引用了AsynchronousFileChannel
的Javadoc:
AsynchronousFileChannel
会关联线程池,它的任务是接收I/O处理事件,并分发给负责处理通道中I/O操作结果的结果处理器。跟通道中发起的I/O操作关联的结果处理器确保是由线程池中的某个线程产生的。
如果在创建AsynchronousFileChannel
时没有为其指明线程池,那就会为其分配一个系统默认的线程池(可能会和其他通道共享)。默认线程池是由AsynchronousChannelGroup
类定义的系统属性进行配置的。
此外还有一种被称为回调的技术。有些开发人员可能会发现回调式用起来更方便,因为它很像Swing、消息和其他Java API中出现过的事件处理技术。
2.5.2 回调式
与将来式相反,回调(callback)式所采用的事件处理技术类似于在Swing UI编程时采用的机制。其基本思想是主线程会派一个侦查员CompletionHandler
到独立的线程中执行I/O操作。这个侦查员将带着I/O操作的结果返回到主线程中,这个结果会触发它自己的completed
或failed
方法(你会重写这两个方法)。
在异步事件刚一成功或失败并需要马上采取行动时,一般会用回调式。比如在读取对盈利计算业务处理至关重要的金融数据时,如果读取失败了,你最好马上就执行回滚操作,或进行异常处理。
在异步I/O活动结束后,接口java.nio.channels.CompletionHandler<V,A>
会被调用,其中V是结果类型,A是提供结果的附着对象。此时必须已经有了该接口的completed(V,A)
和failed(V,A)
方法的实现,你的程序才能知道在异步I/O操作成功完成或因某些原因失败时该如何处理。图2-4展示了这一过程(代码清单2-9是该过程的实现代码)。
图2-4 回调式异步读取
在下例中,你又一次从foobar.txt文件中读取了100 000字节的数据,用CompletionHandler<Integer,ByteBuffer>
声明是成功或是失败。
代码清单2-9 异步 I/O——回调式
try{ Path file = Paths.get(/"/usr/karianna/foobar.txt/") AsynchronousFileChannel channel = AsynchronousFileChannel.open(file); //以异步方式打开文件 ByteBuffer buffer = ByteBuffer.allocate(100_000); /**从通道中读取数据*/ channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer> { /**读取完成时的回调方法*/ public void completed(Integer result, ByteBuffer attachment) { System.out.println(/"Bytes read [/" + result + /"]/"); } public void failed(Throwable exception, ByteBuffer attachment) { System.out.println(exception.getMessage); } });}catch (IOException e){ System.out.println(e.getMessage);}
本节中的两个例子都是基于文件的,但将来式和回调式异步访问也适用于 AsynchronousServerSocketChannel
和AsynchronousSocketChannel
。开发人员可以用它们编写程序来处理网络套接字,比如语音IP或写出性能更优异的客户端和服务器端软件。
接下来的一系列变化统一了套接字和通道,让你可以将套接字和通道交互的管理归结到API中。