前言
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缓冲区
是一个包含了一个数组的对象,除了数组之外,还有几个信息:
- 容量capacity:创建缓冲时确定,不可改变
- 上界limit:缓冲区中现存元素的计数
- 位置position:下一个要被读/写的元素索引,会被get和put自动更新
- 标记mark:备忘位置,需要设定。
有7种缓冲区(除boolean以外的基本类型缓冲区)。
创建缓冲区
1. 静态工厂方法allocate
缓冲区类都是抽象类,但是有定义一个静态工厂方法来创建相应的缓冲区实例。
如:
1 | CharBuffer charBuffer = CharBuffer.allocate(100); |
2. wrap()使用已存在的数组作为缓冲区
1 | char [] myArray = new char[100]; |
复制缓冲区
缓冲区不限于管理数组中的外部数据,也可以管理其他缓冲区的外部数据,这种缓冲区称为视图缓冲器。大多数视图缓冲器都是ByteBuffer的视图。
如下:
1 | ByteBuffer buff = ByteBuffer.allocate(1024); |
Duplicate()复制缓冲区
Duplicate()
创建一个与原始缓冲区相似的新缓冲区,共享数据元素(共用同一个数组,拥有同样的数据视图,并不是真正的复制),拥有相同容量,但是有各自的位置、上界、标记属性。若原缓冲区为只读或直接缓冲,新的缓冲区也会继承这些属性。
对一个缓冲区的数据元素做改变会反映在另一个缓冲区上。
clear()
一般用于需要重新使用缓冲区时的重置pos
1 | CharBuffer charbuffer = CharBuffer.allocate(100); |
flip()
缓冲区反转,一般用于需要重新使用缓冲区时的重置pos
1 | CharBuffer charbuffer = CharBuffer.allocate(100); |
通道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 | SocketChannel socketChannel = SocketChannel.open(); |
connect下也有阻塞和非阻塞模式
阻塞模式下 connect() 会一直等待连接到服务器上连接建立(直接使用socket调用connect也是阻塞的)
非阻塞模式下 connect() 会立即返回值true或false(如果连接不能建立,connect()方法会返回false且异步地继续连接建立的过程。为了确定连接是否建立,可以调用finishConnect()
方法判断是否连接完毕。如下:
1 | socketChannel.configureBlocking(false); |
2. ServerSocketChannel
和ServerSocket大概一致,多了通道语义。
在阻塞模式下,调用ServerSocketChannel的
accept()
会像serverSocket一样一直阻塞直到监听到连接返回一个SocketChannel对象。在非阻塞模式下,在无传入连接等待时,调用
accept()
会立即返回null(从而实现可伸缩性,降低复杂性,实现可选择性)- //,一般会使用选择器来创建ServerSocketChannel对象和监听事件并通知。
1 |
3. DatagramChannel
设置非阻塞模式 configureBlocking(false)
使用configureBlocking()
传参false
即为非阻塞模式,反之为阻塞模式。
可以通过isBlocking()
判断当前socket通道在哪种模式下
创建socket通道 open()
- 调用对应Socket通道类的静态工厂方法
open()
创建 - 创建的时候会返回一个未绑定socket的channel对象,需要后续自己绑定socket
选择器
所有SocketChannel都是可选择的,而FileChannel是不可选择的。一个通道可以被注册到多个选择器上
通道在注册到选择器上之前,要设置为非阻塞模式
选择键SelectionKey
有四个常量:
- OP_READ 1
- OP_WRITE 4
- OP_CONNECT 8
- OP_CONNECT 16
四个值相加的和都不一样,可以通过validOps()
方法返回值确定SelectableChannel支持的操作
通道关闭时,所有对应的选择键会被自动取消,选择器关闭之后,其上通道都会被注销
keys()
返回已注册的键
selectedKeys()
返回已被选择的键
cancel()
返回已经被调用过的键(无效化了)但还没被注销
使用步骤
创建选择器 Selector.open()
通道向选择器注册事件 SelectableChannel.register(Selector, SelectionKey)
*
从选择器中获取事件
根据不同的事件做相应的处理
注册选择器 register()
SelectableChannel.register(Selector, SelectionKey)
该通道必须设置为非阻塞模式!
1 | Selector selector = Selector.open(); |