Java NIO2(AIO) 进行文件异步读取

在  NodeJS 中进行异步操作很简单,而 Java 到了  7 开始才支持异步的 IO 操作。虽然之前的版本有引入非阻塞 IO,但编码中还不易体现出它的优越性。亮一下 NodeJS 用异步 IO 的例子:


var fs = require('fs');
fs.readFile('Test.scala', 'utf-8', function(err, data){
  if( !err ) {
    console.log(data);
  }
});

console.log('continue doing other thins');

执行输出是

continue doing other things
CONTENT FROM FILE Test.scala

对的,理想中的异步操作就是,传递回调函数来读取文件,读取完成后招待回调,且不阻塞主线程。

在 Java 8 之前,因为没有 Lambda 支持只能应用内部类的方式。JDK 提供了以下异步 Channel 来实现异步操作

AsynchronousFileChannel
AsynchronousSocketChannel
AsynchronousServerSocketChannel
AsynchronousDatagramChannel

获取异步结果的方式有 Future 和  CompletionHandler

例子看下两种方式的代码实现:

1. CompletionHandler 回调
 1public static void main(String[] args) throws IOException, InterruptedException {
 2  Path path = Paths.get("Test.Scala");
 3
 4  AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path);
 5  ByteBuffer buffer = ByteBuffer.allocate(100);
 6
 7  asynchronousFileChannel.read(buffer, 0, "attachment information",
 8      new CompletionHandler<Integer, Object>() {
 9
10        @Override
11        public void completed(Integer readCount, Object attachment) {
12          System.out.println(attachment);
13          System.out.println(new String(buffer.array()));
14        }
15
16        @Override
17        public void failed(Throwable exc, Object attachment) {
18          System.out.println("Error:" + exc);
19        }
20      });
21
22  System.out.println("continue doing other things");
23
24  Thread.sleep(1000);
25
26}

参数 Object attachment 可以用来携带你自己需要的信息,上面代码只读取一次。如果大文件需要多次读缓冲,这就要用递归,麻烦些了。

完整的读取文件所有内容的代码可参考:Reading from a file using the AsynchronousFileChannel class

上面代码输出

continue doing other things
attachment information
CONTENT FROM FILE Test.scala

也说明了读取时不会阻断程序继续往下执行,读取就绪后呼叫 CompletionHandler 实例。

2. Future 方式
 1public static void main(String[] args) throws Exception {
 2  AsynchronousFileChannel asynFileChannel =
 3      AsynchronousFileChannel.open(Paths.get("Test.Scala"), StandardOpenOption.READ);
 4  ByteBuffer buffer = ByteBuffer.allocate(100);
 5  Future<Integer> future = asynFileChannel.read(buffer, 0);
 6
 7  while (!future.isDone()) {
 8    System.out.println(new String(buffer.array(), 0, result.get()));
 9  }
10
11}

在 while(!result.isDone()) 处仍然阻碍了主线程继续往下执行。

或者要自己启动线程来处理 future 而影响主线程往下执行,或者要 ExecutorService 帮忙。

个人认为只有像 NodeJS 或  CompletionHandler 那样隐藏线程细节的异步才是真异步编程。又由于 CompletionHandler 不是一个 SAM 类型的接口,所以也就无法直接转换为 Lambda 表达式。

我们可以把 CompletionHandler 拆成两个 SAM 来应用 Java8 的 Lambda 表达式,下面是我做的一个尝试
 1package test;
 2
 3import java.io.IOException;
 4import java.nio.ByteBuffer;
 5import java.nio.channels.AsynchronousFileChannel;
 6import java.nio.channels.CompletionHandler;
 7import java.nio.file.Paths;
 8
 9public class TestNIO2 {
10
11  public static void main(String[] args) throws IOException, InterruptedException {
12
13    read(AsynchronousFileChannel.open(Paths.get("Test.Scala")), ByteBuffer.allocate(100), 0,
14        "start reading", (readCount, buffer, attachment) -> {
15          System.out.println(attachment);
16          System.out.println(new String(buffer.array(), 0, readCount));
17        }, (exc, attachment) -> {
18          System.out.println("Error:" + exc);
19        });
20
21    System.out.println("continue doing other things");
22
23    Thread.sleep(1000);
24
25  }
26
27
28  static <A> void read(AsynchronousFileChannel channel, ByteBuffer buffer, long position,
29      A attachment, final ReadSuccess<A> success, final ReadFailed<A> failure) {
30    channel.read(buffer, 0, attachment, new CompletionHandler<Integer, A>() {
31
32      @Override
33      public void completed(Integer readCount, A attachment) {
34        success.apply(readCount, buffer, attachment);
35      }
36
37      @Override
38      public void failed(Throwable exc, A attachment) {
39        failure.apply(exc, attachment);
40      }
41    });
42  }
43
44}
45
46
47interface ReadSuccess<A> {
48  void apply(Integer readCount, ByteBuffer buffer, A attachment);
49}
50
51
52interface ReadFailed<A> {
53  void apply(Throwable exc, A attachment);
54}

注意,上面的代码仍然只读取了一次数据到 ByteBuffer 中,文件大小超过 100 的字节将读不全,需要进一步递归处理。若只用来读取文件我们可以把  read() 的第二,三位置上的参数 ByteBuffer buffer, Long position 隐藏起来,可完全实现 NodeJS 的调用方式。 永久链接 https://yanbin.blog/java-nio2-aio-readfile/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。