什么是NIO
java.nio全称java non-blocking IO(实际上是 new io),是指JDK 1.4 及以上版本里提供的新api(New IO) ,为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络。
原有的 IO 是面向流的(Stream)、阻塞的,NIO 则是面向块的(Buffer)、非阻塞的。
- 面向流的I/O 系统一次一个字节地处理数据。
- 一个面向块(缓冲区)的I/O系统以块的形式处理数据。
阻塞与非阻塞:
阻塞与非阻塞是描述进程在访问某个资源时,数据是否准备就绪的的一种处理方式。当数据没有准备就绪时:
阻塞:线程持续等待资源中数据准备完成,直到返回响应结果。
非阻塞:线程直接返回结果,不会持续等待资源准备数据结束后才响应结果。
我们可以理解为Channel是两地点之间的铁路,而Buffer是行在其上的火车
Channel
Channel 和 IO中的Stream(流)差不多一个级别的。不过Stream是单向的,如:InputStream , OutputStream ,而Channel是双向的,既可以进行读也可以写操作。
主要有以下四种实现:
- FileChannel 从文件中读取数据
- DatagramChannel 通过UDP读取网络中的数据
- SocketChannel 通过TCP读取网络中的数据
- ServerSockerChannel 监听新进来的TCP连接,类似web服务
Buffer
缓冲区,实际是一个容器,是一个连续数组。Channel提供从文件、网络读取数据的渠道,但是读取和写入数据都要通过Buffer。
上面的图描述了一个客户端向服务端发送数据的过程。
客户端发送数据时,先将数据存入Buffer中,然后将Buffer中的数据写入通道。服务端接收数据从Channel读取到Buffer,在从Buffer中读取出数据。
在NIO中Buffer是一个顶层父类,是一个抽象类,常用子类有;
ByteBuffer,IntBuffer,CharBuffer,LongBuffer等等。
我们拿到一个缓冲区需要进行的操作自然是:将数据写入缓冲区(put)/读取缓冲区数据(get)
一般工作流程如下:
- 写入数据到buffer
- 调用flip方法
- 从buffer中读取数据
- 调用clear方法或者compact方法
Buffer分为读模式和写模式:
Buffer类维护了4个核心变量属性来提供关于其所包含的数组的信息。它们是:
- 容量Capacity缓冲区能够容纳的数据元素的最大数量。容量在缓冲区创建时被设定,并且永远不能被改变。(不能被改变的原因也很简单,底层是数组嘛)
- 在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。写模式下,limit等于Buffer的capacity。当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据。
- Position下一个要被读或写的元素的位置, Position会自动由相应的 get( )和 put( )函数更新。
写数据时,初始的position值为0。当一个byte、long等数据写到Buffer后,position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity-1。读数据时,position也是从0开始,向前读取数据。写模式向读模式切换时,会将position重置为0 - 标记Mark一个备忘位置。用于记录上一次读写的位置。
重要方法
- flip方法:从写模式切换到读模式,position置为0,limit设置为之前写模式的position值。
- rewind方法:将position设置为0,但limit不变
- clear方法:position设置为0,limit设置为capacity的值
- compact方法:将所有未读的数据拷贝到Buffer起始处,然后将position设到最后一个未读元素正后面。limit属性依然像clear方法一样设置成capacity.现在Buffer准备好了写数据,但是不会覆盖未读数据。
- mark、reset方法:通过调用Buffer.mark()方法,可以标记Buffer的一个特定position。之后可以通过调用Buffer.reset方法恢复到这个position
Selector
Selector类是NIO的核心类,Selector能够检测多个注册的通道上是否有时间发生,如果有时间发生,便获取时间然后针对每个事件进行响应的相应处理。这样一来,只是一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大减小了系统开销,不用为每个连接都创建一个线程,不用维护多个线程,并且避免了多线程之间的上下文切换导致的开销。
更加详细参考: 这里