并发 - Java并发 NIO - 同步API

并发 - Java并发 NIO - 同步API

想学会异步先要从同步开始

马上高考成绩快要出来了, 又免不了到了报志愿的时候, 现在小孩和家长如果真的很清楚的话, 志愿也没有什么好选的, 统一都推荐计算机. 实在上不了, 就自学吧.注意猪哥我说的不是指报理科的男生, 而是不论男女, 不论文理. 就连今天刚看的半泽直树, 也是一个自学开发做搜索引擎的例子. 这次就来看看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开头的那些类.
LICENSED UNDER CC BY-NC-SA 4.0
Comment