Java|Java NIO详解


文章目录

  • JAVA NIO
    • 1.NIO与传统IO的对比
    • 2.主要核心原理
      • 2.1缓冲区Buffer(负责数据的存取)
        • 缓冲区的四个核心属性
        • 缓冲区的三个核心操作方法
        • 直接缓冲区和非直接缓冲区
          • 非直接缓冲区
          • 直接缓冲区
          • 直接缓冲区和非直接缓冲区的区别
      • 2.2通道Channel(负责数据的运输)
        • 通道的主要实现类
        • 通道的获取方式
        • 通道之间的数据传输
        • 通道的分散读取和聚集写入
      • 2.3选择器Selector(负责监控通道的IO状况)
        • 选择器使用步骤
        • 选择键SelectionKey
        • Selector常用方法
      • 2.4字符集Charset(编码解码)
        • 编码
        • 解码
    • NIO的网络通信(Selector的核心应用)
      • 三大核心
      • 阻塞与非阻塞
【Java|Java NIO详解】
JAVA NIO java 1.4版本推出了一种新型的IO API,与原来的IO具有相同的作用和目的;可代替标准java IO,只是实现的方式不一样,NIO是面向缓冲区、基于通道的IO操作; 通过NIO可以提高对文件的读写操作。基于这种优势,现在使用NIO的场景越来愈多,很多主流行的框架都使用到了NIO技术,如Tomcat、Netty、Jetty等;所以学习和掌握NIO技术已经是一个java开发的必备技能了。
1.NIO与传统IO的对比
NIO IO
面向缓冲区Buffer 面向流Stream
双向(基于通道Channel) 单向(分别建立输入流、输出流)
同步非阻塞(non-blocking) 同步阻塞
选择器(Selector,多路复用)
支持字符集编码解码解决方案,支持锁,支持内存映射文件的文件访问接口
2.主要核心原理 主要包括:缓冲区(Buffer)、通道(Channel)和选择器(Selector)、字符集(Charset);首先获取用于连接IO设备的通道channel以及用于容纳数据的缓冲区,利用选择器Selector监控多个Channel的IO状况(多路复用),然后操作缓冲区,对数据进行处理。
NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道,即一个单独的线程现在可以管理多个输入和输出通道。
2.1缓冲区Buffer(负责数据的存取)
在javaNIO中负责数据的存取,底层缓冲区就是数组,用于存储不同数据类型的数据,根据不同的数据类型(Boolean除外),提供了相应类型的缓冲区:ByteBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer、CharBuffer。这7种数据类型的Buffer都是通过allocate获取非直接缓冲区或allocateDirect(ByteBuffer通过此方式创建)或wrap(除ByteBuffer意外其他的创建方式)获取直接缓冲区域,分配一个指定大小的缓冲区。
代码实例:Java NIO之缓存Buffer代码实例
缓冲区的四个核心属性
  1. capacity:容量,表示缓冲区的最大容量,一旦声明就不能改变
  2. limit:界限,缓冲区中可以操作数据的大小(limit后面的数据不能读写)
  3. position:位置,表示缓冲区中正在操作数据的位置。
  4. mark:标志,表示记录当前position的位置,可以通过reset()恢复到mark的位置。
    四者的关系:0
缓冲区的三个核心操作方法
  1. put():存数据到缓存区,写数据模式。
  2. flip():切换到读数据模式(position和limit改变,capacity不变)
  3. get():从缓冲区中拿数据。
直接缓冲区和非直接缓冲区 非直接缓冲区 Java|Java NIO详解
文章图片

通过:static ByteBuffe allocate(int capacity)创建指定大小的缓冲区,在JVM内存中创建,在每次调用基础操作系统的一个本机IO之前或者之后,虚拟机都会将缓冲区的内容复制到中间缓冲区(或者从中间缓冲区复制内容),缓冲区的内容驻留在JVM内,因此销毁容易,但是占用JJVM内存开销,处理过程中有复杂的操作。
直接缓冲区 Java|Java NIO详解
文章图片

通过:static ByteBuffer allocateDirect(int capacity)字节Buffer创建指定大小的缓冲区,其他类型的Buffer通过wrap()方法创建缓冲区;在JVM内存外开辟空间,在每次调用基础操作系统的一个本机IO之前或者之后,虚拟机都会避免将缓冲区的内容复制到中间缓冲区(或者从缓冲区中复制内容),缓冲区的内容驻留在屋里内存中,少一次复制过程,如果需要循环使用缓冲区,用直接缓冲区可以很大地提高性能;虽然直接缓冲区可以使JVM进行高效的I/O操作,但它使用的内存使操作系统分配的,绕过了JVM堆栈,建立和销毁比堆栈上的缓存区要更大的开销。
直接缓冲区和非直接缓冲区的区别
  1. 字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
  2. 直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
  3. 直接字节缓冲区还可以通过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer 。 Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。
    字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 Buffer.isDirect() 方法来确定。提供此方法是为了能够在性能关键型代码中执行显式缓冲区管理
2.2通道Channel(负责数据的运输)
Channel表示到IO设备(如:文件、套接字)的连接,即用于源节点与目标节点的连接,在java NIO中Channel本身不负责存储数据,主要是配合缓冲区,负责数据的传输。
代码实例:Java NIO之通道Channel代码实例
Java|Java NIO详解
文章图片

通道的主要实现类
  1. FileChannel类:本地文件IO通道,用于读取、写入、映射和操作文件的通道。
  2. SocketChannel类:网络套接字IO通道,TCP协议,针对面向流的连接套接字的可选择通道(一般用在客户端)。
  3. ServerSocketChannel类:网络通信IO操作,TCP协议,针对面向流的监听套接字的可选择通道(一般用于服务端)。
  4. DatagramChannel类:针对面向数据报套接字的可选择通道,能够发送和接受UDP数据包的Channel。UDP协议,由于UDP是一种无连接的网络协议,只能发送和接受数据包。
    以上几个类都实现了java.nio.channels.Channel接口。
通道的获取方式
  1. java针对支持通道的类提供了getChannel()方法:
    本地文件IO的Channel类有:FileInputStream/FileOutStream,RandomAccessFile。
    网络套接字IO的Channel类:SocketChannel、ServerSocketChannel、DatagraSocket。
  2. 在JDK7.0中的NIO2针对各个通道提供静态方法open()
  3. 在JDK7.0中的NIO2的Files工具类的newByteChannel()
通道之间的数据传输 使用Channel的实现类的对应方法(在直接缓冲区):transferForm()和transferTo()
通道的分散读取和聚集写入
  1. 分散读取:将通道的数据读取到多个缓冲区buffer中。方法:channel.read()
  2. 聚集写入:将多个缓冲区的数据聚集写道通道channel中。方法:channel.write()。
2.3选择器Selector(负责监控通道的IO状况)
是selectableChannel的多路复用器,用于监控SelectableChannel的IO状况。利用selector可以实现在一个线程中管理多个通道Channel,selector是非阻塞IO的核心。
SelectableChannel的结构图:
Java|Java NIO详解
文章图片

选择器使用步骤
  1. 创建selector
    通过调用Selector.open()方法创建一个Selector。
  2. 向选择器注册通道
    注册之前,先设置通道为非阻塞的,channel.configureBlocking(false); 然后再调用SelectableChannel.register(Selector sel,int ops)方法将channel注册到Selector中;其中ops的参数作用是设置选择器对通道的监听事件,ops参数的事件类型有四种(可以通过SelectionKey的四个常量表示):
    (1)读取操作:SelectionKey.OP_READ,数值为1.
    (2)写入操作:SelectionKey.OP_WRITE,数值为4.
    (3)socket连接操作:SelectionKey.OP_CONNECT,数值为8.
    (4)socket接受操作:SelectionKey .OP_ACCEPT,数值为16.
    若注册时不止监听一个事件,可以使用“| 位或”操作符连接。
选择键SelectionKey 表示SelectableChannel在Selector中的注册的标志,每次向选择器注册通道的时候就会选择一个事件(以上四种事件类型)即选择键,选择键包含两个表示位整数值的操作集(分别为interst集合和ready集合),操作集的每一位都表示该键的通道所支持的一类可选择操作。
方法/属性 描述
interset集合 Selector感兴趣的集合,用于指示选择器对管道关心的操作,可通过SelectionKey对象的interestOps()获取;最初,该兴趣集合是通过通道被注册到Selector时传进来的值。该集合不会被选择器改变,但是可以通过interestOps()改变,我们可以通过以下方法判断Selector是否对Channel的某种事件感兴趣:int interestSet=selectionKey.interestOps(); boolean isInterestedInAccept =(interestSet&SelectionKey.OP_ACCEPT)==SelectionKey.OP_ACCEPT
read集合 通道已经就绪的操作的集合,表示一个通道准备好要执行的操作了,可通过SelectionKey对象的readOps()来获取相关通道已经就绪的操作。它是interest集合的子集,并且表示interest集合中从上次调用select()以后已经就绪的那些操作。 //int readSet=selectionKey.readOps(); selectionKey.isAcceptable(); //等价于selectionKey.readyOps()SelectionKey.OP_ACCEPT;selectionKey.isConnectable(); selectionKey.isReadable(); selectionKey.isWritable();
int interestOps() 获取感兴趣事件集合
int readyOps() 获取通道已经准备就绪的操作的集合
SelectableChannel channel() 获取注册通道
Selector selector() 返回选择器
boolean isReadable() 检查Channel中读事件是否就绪
boolean isWriteable() 检测Channel 中写事件是否就绪
boolean isConnectable() 检测Channel中连接是否就绪
boolean isAcceptable() 检测Channel中接收是否就绪
Selector常用方法
方法 描述
Set< SelectionKey > keys() 所有的SelectionKey集合,代表注册在该Selector上的Channel
selectedKeys() 被选择的SelectionKey集合。返回此Selector的已选择键集
int select() 监控所有注册的Channel,当它们中间有需要处理的IO操作时,该方法返回,并将对应的SelectionKey加入被选择的SelectionKey集合中,该方法返回这些Channel的数量。
int select(long timeout) 可以设置超时时长的select()操作
int selectNow() 执行一个立即返回的select()操作,该方法不会阻塞线程
Selector wakeUp() 使一个还未返回的select()方法立即返回
void close() 关闭该选择器
2.4字符集Charset(编码解码)
代码实例:Java NIO字符集CharSet代码实例
编码 字符串转成字节数组
解码 字节数组转成字符串
NIO的网络通信(Selector的核心应用) 三大核心
  1. 通道(channel):负责管道节点的连接及数据的运输
  2. 缓冲区(buffer):负责数据的存取
  3. 选择器(selector):是selectableChannel的多路复用器,用于监控SelectableChannel的IO状况。
阻塞与非阻塞
  1. 传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write()
    时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不
    能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会
    阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,
    当服务器端需要处理大量客户端时,性能急剧下降。
  2. Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数
    据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时
    间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入
    和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同
    时处理连接到服务器端的所有客户端。

    推荐阅读