马上高考成绩快要出来了, 又免不了到了报志愿的时候, 现在小孩和家长如果真的很清楚的话, 志愿也没有什么好选的, 统一都推荐计算机. 实在上不了, 就自学吧.注意猪哥我说的不是指报理科的男生, 而是不论男女, 不论文理.

就连今天刚看的半泽直树, 也是一个自学开发做搜索引擎的例子. 这次就来看看NIO的异步内容.

  1. NIO的N之处
  2. Buffers类简介
  3. Channel类简介
  4. NIO的基础操作逻辑
  5. 缓冲区的更多细节

NIO的N之处

来看看NIO的所有新组件, 也就是Buffer, Channel和Selector以及辅助类Charset.

这里直接就看官网文档, 最正规核心. Java NIO的API有如下:

  1. Buffers, 表示存储数据的容器, 这些类定义在java.nio包内部
  2. Charsets, 表示一些解码器和编码器, 用于辅助字节和Unicode之间的转换, 主要用于字符处理. 这些类定义在java.nio.charset包中
  3. Channels, 表示连接到可以对其进行I/O操作的对象, 其实也可以认为Channel就代表那些可以进行I/O的对象, 比如文件, UDP, Socket等. 这些类定义在java.nio.channels中.
  4. Selectors, 还有与之搭配的Selector keys, 与Channel中的一类selectable channels 合并起来实现非阻塞IO. 其实就是I/O多路复用. selector的所有API与Channel结合紧密, 所以也位于java.nio.channels中.

NIO的部分有阻塞也有非阻塞的部分, 非阻塞的部分需要使用支持Selector的Channel类来实现, 普通的Buffer-Channel则对应阻塞IO.

除了上边几个nio的子包之外, 还有一个子包就是上一次看过的java.nio.file包了.

Buffers类简介

Buffers实际上就是字面意思的缓冲区, 一个Buffers会由一个Channel来写入, 或者向Buffers中写入, 在最终会被写入到一个Channel中.

在java.nio中定义了所有基本类型对应的Buffer类型:

  1. ByteBuffer
  2. CharBuffer
  3. DoubleBuffer
  4. FloatBuffer
  5. IntBuffer
  6. LongBuffer
  7. ShortBuffer

此外还有针对一个内存映射文件的MappedByteBuffer.

这些具体Buffer类都继承自java.nio.Buffer类, 并且实现了Comparable<自身类型>接口.

要创建一个缓冲区对象, 可以使用缓冲区对象的allocate()方法, 其中可以传入一个大小, 表示对应数据类型的多少大的一个空间:

//分配42字节的缓冲区
ByteBuffer buf = ByteBuffer.allocate(42);

//分配999个short长度的缓冲区空间
ShortBuffer sb = ShortBuffer.allocate(999);

读写缓冲区的方法如下:

  1. get(), 读入对应的一个数据类型的值
  2. put(), 写入一个对应数据类型的值
  3. get(xxx[] dst), 读出一批数据到指定的目标数组
  4. get(xxx[] dst, int offset, int length), 上一个方法的重载, 可以指定偏移量和长度
  5. put(xxx[] src), 将指定目标数组的数据写入缓冲区
  6. put(xxx[] src, int offset, int length), 上一个的重载, 可以指定偏移量和长度
  7. channel.read(buffer), 用channel来向buffer中读出数据, 对于程序来说其实就是读出Channel中的内容. 这个方法返回读出的长度, 如果是-1, 则表示没有数据被读入.
  8. channel.write(buffer), 用channel来向buffer中读出数据, 对于程序来说其实就是写入Channel中的内容. 同样返回写入数据的长度.

这其中的bytebuffer有一些特殊的方法, 就是将其中的内容作为任意的基本类型读出, 了解即可. 大多数IO其实都使用ByteBuffer, 很少有专门读其他类型的Buffer.

除了读写之外, 缓冲区有一个flip()方法很重要, 涉及到内部的状态变量.每个缓冲区的内部有重要的状态变量, 就是记录缓冲区的的状态, 这些状态包括position=已经写了多少数据/下一个字节放到缓冲区的哪里.

limit = 还有多少数据需要取出或者还有多少数据需要放入, postion一定会小于等于limit

capacity = 缓冲区的总长度

可以将缓冲区想象成为一个被包装过的数组, 那么position就是指向下一个可读取位置的索引, 而limit至少是已存在数据的最后一个索引. 而capacity就是整个数组的长度.

这里的IBM网站讲解的很详细.
在缓冲区向外输出的时候, 需要先调用flip()方法, 读完成后, 需要调用clear()方法, 这些方法都是重设内部的状态变量.

由此可见, 缓冲区的操作其实倒有点像C或者C++的基本I/O函数, 感觉还是万变不离其宗.

Channel类简介

Channel的文档见此..由于一个Channel对应一个I/O对象, 所以可以将Channel认为就是一个I/O对象. Channel对象可以来自于文件, UDP和TCP等网络通信等.

要创建Channel对象, 需要从一个I/O流中创建Channel对象, 常见的Channel对象有这么几种:

  1. DatagramChannel, 一看名字就知道, 用于UDP通信
  2. ServerSocketChannel, 服务端的TCP通信
  3. SocketChannel, 客户端的TCP通信
  4. FileChannel, 用于文件
  5. SelectableChannel
  6. SelectionKey
  7. Selector这三个是Select家族, 需要搭配使用来实现I/O多路复用.
  8. AsynchronousChannelGroup
  9. AsynchronousFileChannel
  10. AsynchronousServerSocketChannel
  11. AsynchronousSocketChannel四个异步家族, 多路复用时候每个线程的I/O依然是阻塞, 这个异步可以让内部的I/O也不阻塞, 有点像Future.

现在先不涉及网络编程, 看看本地的FileChannel使用.

点开FileChannel的类, 可以发现通过FileInputStream.getChannel(), FileOutputStream.getChannel(), RandomAccessFile.getChannel()可以创建FileChannel.

下边就可以来尝试一下编写NIO的读写操作了.

NIO的基础操作逻辑

NIO的基础操作逻辑如下:

  1. 通过流创建Channel对象
  2. 从Channel对象中将数据读入缓冲区
  3. 使用flip()让缓冲区处于就绪状态
  4. 使用缓冲区的数据, 比如打开另外一个Channel, 进行写入.
  5. 如果没有读完和写完, 重复2-3步骤
  6. .clear()清除缓冲区, 准备好下一次读入
  7. 关闭所有Channel

这里再来试验一下, 用NIO如何进行操作.写一个简单的复制文件的程序:

//从源文件中创建channel
FileInputStream fileInputStream = new FileInputStream("D:\\downloads\\music\\王菲\\2000《寓言》内地引进版\\寓言 内地引进版.cue");
FileChannel channel = fileInputStream.getChannel();

//从目标文件创建channel
FileChannel newChannel = new FileOutputStream(new File("new.txt")).getChannel();
ByteBuffer buffer = ByteBuffer.allocate(100);

while (channel.read(buffer) != -1) {
    //做好写入准备
    buffer.flip();
    newChannel.write(buffer);
    //写入完成后清除文件
    buffer.clear();
}

channel.close();
newChannel.close();

程序里就是按照上边的逻辑来进行操作, 每次也可以将缓冲区的内容转换成数组进行操作. 这其实就是C语言基础的IO模式.

缓冲区的更多细节

这里就简单记录一下, 需要用到的时候回来再看:

  1. slice() 方法根据现有的缓冲区创建子缓冲区, 与原来缓冲区共享数据
  2. asReadOnlyBuffer()将缓冲区转换为只读缓冲区, 其实返回的是一个与原来缓冲区共享的缓冲区, 但只读
  3. ByteBuffer可以设置为直接或者间接缓冲区, 如果是直接缓冲区, JDK尽量会使用操作系统原生的方法直接来操作缓冲区
  4. 内存映射文件也可以作为缓冲区的来源, 搭配Channel使用, 比如: MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE, 0, 1024 );

此外还有分散和聚集, 就是用缓冲区数组来当成缓冲区, 对于把数据分段处理很有效果.

到目前为止, 使用的NIO都是同步的, 也就是说read()和write()方法都会阻塞.

可见, 对于处理字节之类的, 使用NIO要方便很多, 如果要处理字符的话, NIO其实还不怎么方便, 因为固定长度的缓冲区很可能没法读出像UTF-8这种编码的完整字符. 不过nio为此也提供了Path, Paths和Files来进行辅助, 可以简单的操作一些不长的文本文件.

下一次就来看看NIO真正的全新之处, 就是类似于I/O多路复用的Selector, 然后也来看看异步的I/O操作也就是Async开头的那些类.