Java-NIO

前言

NIO面向块(整个缓冲区)
有所有基本类型的Buffer支持
提供新的I/O抽象:Channel
支持锁和内存映射文件的文件访问接口
多路复用非阻塞式的高伸缩性I/O(原来的是阻塞的)🖊

NIO的特性

NIO面向块(整个缓冲区)
有所有基本类型的Buffer支持
提供新的I/O抽象:Channel
支持锁和内存映射文件的文件访问接口
多路复用非阻塞式的高伸缩性I/O(原来的是阻塞的)

NIO的目的

提供高速的I/O而无需编写自定义的本机代码。NIO将最耗时的I/O操作(填充和提取缓冲区)转移到操作系统中,极大提高了运行速度。

原来的I/O是通过流一个一个字节输出和读取的,而NIO是一次产生或消费一个数据块,比流更快。

阻塞、非阻塞、同步、异步

阻塞和非阻塞:针对进程在访问数据时,数据是否已经准备就绪的的一种处理方式

阻塞:数据还没准备好时,要等待缓冲区中的数据准备好过后才能处理其他的事情,否则一直等在那里。

非阻塞:当进程访问数据缓冲区时,数据没准备好的话,直接返回不是要等待,有数据时也直接返回。

异步和同步:针对应用和操作系统处理锁采用的方式

如:同步需要应用程序直接参与到IO读写的操作,而异步IO交给操作系统处理,程序可以做其他的事情。同步IO必须阻塞在某个方法上等待IO完成

NIO采用轮询模式(select模式(多路复用模式))这是个同步非阻塞的模式,把读写时间交给单个线程处理,不断轮询读写缓冲区,检查数据是否已经准备好。之后再通知相应的读写线程。这样的话其他线程就可以做其他事情,只阻塞select的线程。

NIO缓冲区

是一个包含了一个数组的对象,除了数组之外,还有几个信息:

  1. 容量capacity:创建缓冲时确定,不可改变
  2. 上界limit:缓冲区中现存元素的计数
  3. 位置position:下一个要被读/写的元素索引,会被get和put自动更新
  4. 标记mark:备忘位置,需要设定。

有7种缓冲区(除boolean以外的基本类型缓冲区)。

创建缓冲区

1. 静态工厂方法allocate

缓冲区类都是抽象类,但是有定义一个静态工厂方法来创建相应的缓冲区实例。
如:

1
CharBuffer charBuffer = CharBuffer.allocate(100);

2. wrap()使用已存在的数组作为缓冲区

1
2
char [] myArray = new char[100];
CharBuffer charbuffer = CharBuffer.wrap(myArray);

复制缓冲区

缓冲区不限于管理数组中的外部数据,也可以管理其他缓冲区的外部数据,这种缓冲区称为视图缓冲器。大多数视图缓冲器都是ByteBuffer的视图。

如下:

1
2
ByteBuffer buff = ByteBuffer.allocate(1024);
IntBuffer intBuffer = buff.asIntBuffer();

Duplicate()复制缓冲区

Duplicate()创建一个与原始缓冲区相似的新缓冲区,共享数据元素(共用同一个数组,拥有同样的数据视图,并不是真正的复制),拥有相同容量,但是有各自的位置、上界、标记属性。若原缓冲区为只读或直接缓冲,新的缓冲区也会继承这些属性。

对一个缓冲区的数据元素做改变会反映在另一个缓冲区上。

clear()

一般用于需要重新使用缓冲区时的重置pos

1
2
CharBuffer charbuffer = CharBuffer.allocate(100);
charbuffer.claer(); // pos = 0;

flip()

缓冲区反转,一般用于需要重新使用缓冲区时的重置pos

1
2
CharBuffer charbuffer = CharBuffer.allocate(100);
charbuffer.flip(); // limit = pos; pos = 0;

通道Channel

Channel用于字节缓冲区和位于通道的另一个实体(通常是一个文字或者一个套接字)之间有效的传输数据。
通道是一种途径,是访问系统I/O服务的导管。借助通道可以用最小的开销访问操作系统本身的I/O服务。缓冲区则是通道内部用来发送和接收数据的端点。

通道的API由接口指定,具体的实现是native的,不同的操作系统的通道实现有根本的差异,但都是直接访问操作系统底层的I/O服务。

通道基础

  • 通道可以以阻塞或非阻塞模式进行。(只有面向流的通道像sockets和pipie才能使用非阻塞模式,而文件通道总是阻塞的)
    • 非阻塞模式不会让调用的线程休眠。请求的操作要么立即完成,要么返回一个结果表明没进行操作。
  • 通道会连接一个特定I/O服务且通道实例的性能受限于它所连接的I/O服务特征。如:一个连接到只读文件的Channel实例不能进行写操作

关闭通道 close()

我们可以通过调用close()方法来关闭通道;

一个打开的通道代表与一个特定I/O服务的特定连接,并封装该连接的状态。当通道关闭时,这个连接会丢失,然后通道将不再连接任何东西。

可以通过isOpen()方法来判断通道是否打开,如果对关闭的通道进行读写等操作,会导致ClosedChannelException异常;

另外,如果一个通道实现了InterruptibleChannel接口,那么,当该通道上的线程被中断时,通道会被关闭,且该线程会抛出ClosedByInterruptException异常;

Channel分类

I/O服务广义可分为两类:File I/O 和 Stream I/O。Channel也分为了两类:文件File通道和套接字socket通道

文件通道

1. FileChannel

文件通道总是阻塞的,不能被设置为非阻塞模式。

  • FileChannel 对象只能通过在一个打开的文件流对象:RandomAccessFile、FileInpurStream、FileOutputStream对象上调用getChannel() 方法来获取,不能直接创建。getChannel会返回一个连接到相同文件的FileChannel对象且该对象具有与file对象相同的访问权限
  • 通道会连接一个特定I/O服务且通道实例的性能受限于它所连接的I/O服务特征。一个连接到只读文件的Channel实例不能进行写操作
  • FileChannel 对象是线程安全的。(它有多线程的操作,也有单线程串行的操作,像影响文件大小和位置的操作就是单线程串行的,即写锁读锁的问题)

创建文件通道 getChannel()

FileChannel 对象只能通过在一个打开的文件流对象:RandomAccessFile、FileInpurStream、FileOutputStream对象上调用getChannel() 方法来获取,不能直接创建。

使用文件通道 read() write() position()

使用 FileChannel 的 read()write() 方法进行文件访问,配合position() 进行文件操作。

  • FileChannel 位置 (position) 是底层的文件描述符获取的,由所有该文件的FileChannel对象共享可见。

position 决定文件读写的位置,当字节被read() 或 write() 时,position会自动更新。

当read() 方法到了文件大小的值时,会返回文件尾条件值(-1)

write时position超过文件大小时,文件会自动扩展来容纳新字节。

write() 前一般要 flip()

套接字(socket)通道

可设置为非阻塞模式。非阻塞模式使管理维护多个socket通道变得更容易,无需把过多的性能开销耗费在切换管理socket的线程上。

  • Socket通道在实例化时都会创建一个对等的socket对象(对应Socket,ServerSocket,DatagramSocket)

  • Channel可以通过getSocket()获取其对等的socket对象。

  • socket可以通过getChannel()方法获取其对应的Channel(不是所有的socket都会有关联的通道,若使用传统方法(直接实例化)创建一个Socket对象,就不会关联Channel,而且getChannel返回null)

1. SocketChannel

SocketChannel创建的时候会自动串联一个对等的Socket对象。

使用SocketChannel 的connect()方法将通道连接。可以用isConnected()判断通道的连接状态。

1
2
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://127.0.0.1", 80));

connect下也有阻塞和非阻塞模式
阻塞模式下 connect() 会一直等待连接到服务器上连接建立(直接使用socket调用connect也是阻塞的)
非阻塞模式下 connect() 会立即返回值true或false(如果连接不能建立,connect()方法会返回false且异步地继续连接建立的过程。为了确定连接是否建立,可以调用finishConnect()方法判断是否连接完毕。如下:

1
2
3
4
5
6
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://127.0.0.1", 80));

while(! socketChannel.finishConnect() ){ \\ 直到连接成功就退出自旋
//wait, or do something else...
}

2. ServerSocketChannel

和ServerSocket大概一致,多了通道语义。

  • 在阻塞模式下,调用ServerSocketChannel的accept()会像serverSocket一样一直阻塞直到监听到连接返回一个SocketChannel对象。

  • 在非阻塞模式下,在无传入连接等待时,调用accept()会立即返回null(从而实现可伸缩性,降低复杂性,实现可选择性)

  • //,一般会使用选择器来创建ServerSocketChannel对象和监听事件并通知。
1
2


3. DatagramChannel

设置非阻塞模式 configureBlocking(false)

使用configureBlocking()传参false即为非阻塞模式,反之为阻塞模式。

可以通过isBlocking()判断当前socket通道在哪种模式下

创建socket通道 open()

  1. 调用对应Socket通道类的静态工厂方法open()创建
  2. 创建的时候会返回一个未绑定socket的channel对象,需要后续自己绑定socket

选择器

所有SocketChannel都是可选择的,而FileChannel是不可选择的。一个通道可以被注册到多个选择器上

通道在注册到选择器上之前,要设置为非阻塞模式

选择键SelectionKey

有四个常量:

  1. OP_READ 1
  2. OP_WRITE 4
  3. OP_CONNECT 8
  4. OP_CONNECT 16

四个值相加的和都不一样,可以通过validOps()方法返回值确定SelectableChannel支持的操作

通道关闭时,所有对应的选择键会被自动取消,选择器关闭之后,其上通道都会被注销

keys()返回已注册的键

selectedKeys()返回已被选择的键

cancel()返回已经被调用过的键(无效化了)但还没被注销

使用步骤

创建选择器 Selector.open()

通道向选择器注册事件 SelectableChannel.register(Selector, SelectionKey)*

从选择器中获取事件

根据不同的事件做相应的处理

注册选择器 register()

SelectableChannel.register(Selector, SelectionKey)

该通道必须设置为非阻塞模式!

1
Selector selector = Selector.open();

本文标题:Java-NIO

文章作者:Aaron.H

发布时间:2018年09月02日 - 12:09

最后更新:2018年09月07日 - 08:09

原始链接:https://uncleaaron.github.io/Blog/Java/Java-NIO/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。