- java.nio:NIO-2;
NIO
面向流的IO体系一次只能处理一个或多个字节/字符,直至读取所有字节/符,且流中的数据不能前后移动、效率低,当数据源中没有数据时会阻塞线程。Java-4提供的新API,Non-blocking IO(New IO,面向块的IO体系)为所有的原始类型提供Buffer缓存支持,缓冲区中数据可前后移动、灵活性好,非阻塞式允许一个单独的线程可以管理多个输入/出Channel,但是处理和解析数据相对复杂。
采用内存映射文件的方式处理输入输出,将文件或文件的一段区域映射到内存中,模拟OS中虚拟内存的概念,提高IO速度。
区别:
- IO:阻塞式IO,面向流、基于字节/字符流;
- NIO:非阻塞式IO,面向块(缓冲区)、基于通道/缓冲区,选择器,异步,双向;
Sun官方标榜的特性:
- 为所有的原始类型提供缓存(Buffer)支持;
- Java.nio.charset:字符集编码解码解决方案;
- Channel:一个新的原始I/O抽象;
- 支持锁和内存映射文件的文件访问接口;
- 提供多路非阻塞式(non-bloking)的高伸缩性网络I/O;
思路:分而治之、专人入则专门的任务,多路复用IO,解决处理速度的差异,避免阻塞带来的多进/线程间的上下文切换。
工作原理:
- 由一个专门的线程来处理所有的IO事件并负责分发;
- 事件驱动机制:事件到达时触发而不是同步的去监视事件;
- 线程通讯:线程之间通过wait、notify等方式通讯,保证每次上下文切换都是有意义的、减少无谓的线程切换;
注:每个线程的处理流程大概都是读取数据、解码、计算处理、编码、发送响应。
NIO的核心的API:Channel,Buffer,Selector
Channel
通道,对传统IO系统的模拟,传输数据,但是双向的、可读可写,支持异步I/O、支持分散/聚集(scatter/gather)操作。
- FileChannel:阻塞式,读写文件;
- Socket通道:
- SocketChanel:阻塞/非阻塞式,客户端,发起到监听服务器的连接、通过TCP向网络连接的两端读写数据,有效操作集合是read、write和 connect;
- ServerSocketChannel:阻塞/非阻塞式,服务器,基于通道的socket监听器、能够监听客户端发起的TCP连接并为每个TCP连接创建一个新的SocketChannel用于数据读写,有效操作集合是accept;
- DatagramChannel:以UDP向网络连接的两端读写数据,无连接的、可以“连接”到网络中的特定地址,既可作为服务器也可作为客户端,发送和接收的是数据包;
函数:
- getChannel():通过IO流对象获得Channel对象;
- read()/write():读写Channel操作;
通道间的数据传输:前提是有一个通道是FileChannel
- transFrom():otherChannels to FileChannel,将数据从源通道传输到FileChannel中;
- transTo():FileChannel to otherChannels,将数据从FileChannel传输到其他通道;
scatter/gather:矢量I/O
- 分散: Scattering Reads,将数据从一个Channel读取(分散)到多个Buffer中。不适用于动态消息的填充,因为只有前一个Buffer填满之后才能填充下一个Buffer。
- 聚集: Gathering Writes,将数据从多个Buffer中写入(聚集)到一个Channel中。适于处理动态消息。
分散-聚集I/O对于要将数据划分为几个部分时很有用,有助于实现复杂的数据格式。
参考:白话NIO之Channel;
Buffer
缓冲区,连续内存块、本质是数组,支持多种数据类型,数据读写中转站,提供对数据的结构化访问,跟踪系统的读/写进程。顶层父类、抽象类,最常用的是ByteBuffer。
一个buffer主要由position、limit、capacity三个变量控制读写过程:
- position:标识当前数据读写位置;
- limit:可读写的数据量;
- capacity:buffer容量;
三者关系:0<=mark<=position<=limit<=capacity
函数:
- allocate():为Buffer分配空间创建缓冲区
- duplicate():复制缓冲区,2个缓冲区共享数据
- slice():以原始缓冲区从当前位置开始的部分缓冲区创建新缓冲区,缓冲区分片、创建子缓冲区,与缓冲区共享同一个底层数据数组
- asReadOnlyBuffer():常规缓冲区转换为只读缓冲区,返回一个与原缓冲区完全相同的缓冲区并共享数据,不能将只读缓冲区转换为常规缓冲区
- clear():重置属性position=0、limit=capacity,但数据并未清除
- flip():将写模式转换为读模式(重置参数:limit=position、position=0),准备将Buffer中的数据读出并写到Channel中
- rewind():position重置为0,用于重复读
- mark() + reset():标记位置 + 复位到该位置
- compact():将所有未读数据拷贝到Buffer起始处,并重置属性position=最后一个未读元素的下一个位置、limit=capacity,新写数据将放到缓冲区未读数据的后面
- wrap():将数组包装为缓冲区
- equals() + compareTo():比较Buffer
步骤:
- 创建Buffer,分配空间
- 将数据写入到Buffer(写数据)
- 从Channel写到Buffer;
- 通过Buffer的put()方法写到Buffer;
- 调用flip()方法
- 从Buffer中读取数据(读数据)
- 从Buffer中读数据到Channel;
- 通过Buffer的get()方法从Buffer中读数据;
- 调用clear()方法或compact()方法
ByteBuffer
支持字节数组上进行get/set操作。
- get()方法
- put()方法
内存映射文件I/O
一种读和写文件数据的方法,将文件中实际读取或写入的数据映射到内存中,利用虚拟内存技术提供对文件的高速缓存。
- 提供文件内存映射方案、读写性能极高、支持随时随地写入,适于处理超大文件;
- 可以创建直接缓冲区、加快I/O速度;
MappedByteBuffer类,继承于ByteBuffer类。
force():读写模式下对缓冲区内容修改,强行写入文件;
load():将缓冲区内容载入内存,并返回该缓冲区的引用;
isLoaded():判断缓冲区内容是否在物理内存中;
MappedByteBuffer map(int mode, long position, long size);
mode:可访问该内存映像文件的方式
- READ_ONLY;
- READ_WRITE;
- PRIVATE;
参考:白话NIO之Buffer;
Selector
选择器,抽象类,非阻塞式、支撑NIO的多路复用,基于Peactor模式的工作方式,实现非阻塞I/O的核心对象是Selector,Selector提供了询问通道是否已经准备好执行I/O操作的能力。允许单线程监听和处理多个Channel的事件。通过注册Channel事件到Selector,再调用方法select()获取到达的事件并对事件响应处理。当有读/写等注册事件到达时通知Selector,从Selector中可以获得相应的(选择)键SelectionKey,通过SelectionKey可以找到发生的事件和该发生事件的SelectableChannel,进而收发数据。
Using a Selector,manage and handle multiple channels with a single thread,while the Channel must be in non-blocking mode.
一个Selector可以注册多个Channel,一个Channel也可以同时注册到多个Selector中,但是一个Selector中同一个Channel只能有一个。服务端和客户端各自维护一个Selector对象管理通道。Selector和通道无直接,而是通过SelectionKey对象联系。
注:Selector对于打开了多个连接(通道)、但每个连接的流量都很低的情况特别适用。
参考:NIO全解说明 | AppZone - Selector及例子分析;
Selector
public abstract class Selector
{
public static Selector open() throws IOException
public abstract boolean isOpen();
public abstract void close() throws IOException; // 关闭Selector且使注册到该Selector上的所有SelectionKey实例无效,但通道本身不会关闭 // 三个集合
public abstract Set keys(); // 返回与Selector关联的已注册的键(包括已经失效的键)的集合
public abstract Set selectedKeys(); // 返回与Selector关联的已选择的键的集合(已注册集合的子集)
private abstract Set canceledKeys(); // 返回与Selector关联的已取消的键的集合(已注册集合的子集)
// 三个select()方法(区别:所注册的通道在当前都没有就绪时是否阻塞)
public abstract int select() throws IOException; // 没有通道就绪时阻塞
public abstract int select (long timeout) throws IOException; // 超时限制,timeout为0时等效select()
public abstract int selectNow() throws IOException; // 不阻塞,当前没有通道就绪,立即返回0 public abstract void wakeup(); // 使因调用select()方法阻塞的线程立即返回(唤醒线程)
public abstract SelectionProvider provider();
}
选择键的集合
- keys
所有已注册且没有被deregister的选择键的集合,Set类型。(已经cancelled但未deregister)。不能直接修改;
- selectedKeys
已选择键的集合,keys的子集,Set类型。上一次操作期间被Selector判断为已经准备就绪的通道所对应的选择键。支持remove、不支持add。
- canceledKeys
已取消键的集合,keys的子集,Set类型。已经被取消但尚未被取消注册关系的选择键。Selector对象的私有成员,不可直接访问。
在Selector选择期间,register()为集合keys添加元素,通道的close()或者键的cancel()均会将键移加到集合canceldKeys中。若集合canceldKeys非空,在下一次Selector选择期间,deregister()会清除该集合中的键并将该键从集合keys或selectedKeys中移除。select()对集合keys和selectedKeys都会同步,多线程调用会被阻塞,select()对集合canceledKeys并未同步,导致selectedKeys中的key可能是(在选择期间)已取消的,注意校验:key.isValid() && key.isReadable()。
select()方法
返回值表示自上次调用select()方法后才变成就绪状态的通道。
SelectionKey
一个SelectionKey表示一个到达事件,封装特定通道和对应Selector的注册关系。SelectionKey对象是线程安全的,由Selector维护,表示SelectableChannel在Selector中注册的标识。
public abstract class SelectionKey
{
// 操作事件
public static final int OP_READ;
public static final int OP_WRITE;
public static final int OP_CONNECT;
public static final int OP_ACCEPT; public boolean isAcceptable();
public boolean isConnectable();
public boolean isReadable();
public boolean isWritable(); public abstract SelectableChannel channel(); // 返回注册的通道对象
public abstract Selector selector(); // 返回Selector对象
public abstract void cancel(); // 终结注册关系
public abstract boolean isValid(); // 注册关系有效性检查
// 两个以整数形式进行编码的byte掩码
public abstract int interestOps(); // 指示通道/选择器组合体关心的操作:instrest 集合
public abstract void interestOps (int ops); // 设置interest集合
public abstract int readyOps(); // 指示通道已经准备好要执行的操作:ready集合(interest集合的子集)
// 附加额外信息,区别Channel(附加对象必须人为清除,GC不会回收)
public final Object attach(Object ob)
public final Object attachment()
}
监听事件:
- Connect:a Channel that has connected successfully to another Server,客户端连接服务端事件 ;
- Accept:a Server Socket Channel which accepts an incoming connection,服务端接收客户端连接事件;
- Read:a Channel that has data ready to be read;
- Write:a Channel that is ready to write data to it;
事件集合:
- interest集合
Selector感兴趣的集合,集合不会被选择器改变,但可以通过interestOps(int ops)更新,不影响此次选择过程、在下一次选择过程中生效。
- ready集合
通道已经就绪的操作的集合,表示interest集合中从上次select()调用以后才就绪的那些操作,元素是累积的,集合通过Selector更新,但不会被外界改变。
延迟注销:
cancel()方法,键被取消且立即失效、被存放在Selector的已取消的键的集合里,当再次调用select()方法时,清理已取消的键的集合中被取消的键同时终结注册关系。
- 防止线程在取消键时阻塞;
- 防止与正在进行的选择操作冲突;
SelectableChannel
可选择通道,抽象类、是所有支持就绪检查的通道类的父类,提供了实现通道的可选择性所需要的公共方法。
- FileChannel:没有继承SelectableChannel,不可选择;
- Socket通道:继承SelectableChannel,可选择;
- Pipe管道:支持可选择;
public abstract class SelectableChannel extends AbstractChannel implements Channel
{
public abstract SelectionKey register(Selector sel, int ops) throws ClosedChannelException;
public abstract SelectionKey register(Selector sel, int ops, Object attach) throws ClosedChannelException;
public abstract boolean isRegistered();
public abstract SelectionKey keyFor(Selector sel); // 返回与该通道和指定的选择器相关的键
public abstract int validOps(); // 获取通道的有效的可选择的操作事件集合
}
选择过程
选择器类的核心是选择过程。当Selector的选择操作select()被调用时:
1.检查已被取消的键的集合
若非空,清空已取消键集合,并且集合中的键将从另外两个集合中移除且相关通道会被注销。
2.检查已注册的键的集合中的键的interest集合
对于没有准备好操作的通道将不会执行任何操作,对于已经准备好interest集合中至少一个操作的通道,执行:
- 通道的键未在已选择的键的集合中,键的ready集合被清空,然后(表示操作系统发现的当前通道已经准备好的操作的)比特掩码将被重置;
- 通道的键已在已选择的键的集合中,键的ready集合被(表示操作系统发现的当前通道已经准备好的操作的)比特掩码更新,所有之前的已经不再是就绪状态的操作不会被清除;
注:步骤2对interest集合的改动不会影响剩余的检查过程。其中,比特位只会被设置、不会被清理。
3.步骤2完成后,重新执行步骤1,注销在选择过程中任意一个键已经被取消的通道
.select()操作返回的值是ready集合在步骤2中被修改的键的数量,而不是已选择键的集合中的通道总数。返回值不是已准备好的通道总数,而是从上一个select()调用之后进入就绪状态的通道数量,之前调用中就绪的通道且在本次调用中仍然就绪的和不再处于就绪状态的通道均不会被计入;
Selector执行选择过程,系统底层会依次询问每个通道是否已就绪(会耗时),该过程可能会造成调用线程进入阻塞状态,通过三种方法可以唤醒在select()方法中阻塞的线程:
- 调用Selector对象的wakeup()方法让处于阻塞状态的select()方法立刻返回
选择器上第一个还未返回的选择操作立即返回。若当前没有进行中的选择操作,那么下一次对select()方法的调用将立即返回。
- Selector对象的close()方法关闭Selector对象
任何一个在选择操作中阻塞的线程均被唤醒(类似wakeup()),同时注册到该Selector的所有Channel被注销、所有键被取消,但Channel本身并不会关闭。
- 调用interrupt()方法
睡眠的线程会抛出InterruptException异常,选择器类捕获该异常后再调用wakeup()方法。
管道 - Pipe
2个线程间的单向数据连接。Pipe有一个source通道和一个sink通道,数据被写到sink通道(write()方法)、从source通道(read()方法)读取。
参考:
NIO其他
NIO的主要应用在高性能、高容量服务端应用程序。目前最知名的开源Java NIO框架要属Mina和Netty。其中,Netty的设计对开发者有更友好的扩展性,且性能方面优于Mina,同时Netty有完善的文档。
IO和NIO对应用程序设计的影响
- 对IO或NIO类的API调用;
- 数据处理;
- 用来处理数据的线程数;
文件锁定
Java从JDK-1.4的NIO开始支持文件锁。Java 7对其进行了改进,新增1个Path接口和2个工具类Files、Paths。
劝告式的(advisory)锁,通过锁的共享或排他有效处理多个进程对同一文件的并发访问。
FileLock fileLock = fileChannel.lock()/tryLock(); // 阻塞式/非阻塞式,默认获取排它锁
fileLock.release(); // 释放锁
文件监控
NIO.2的Path类新增WatchService类用于监听文件系统的变化。
register(WatchService watcher, WatchEvent.Kind<?>...events);
// 获取监听的事件
WatchKey poll(); // 非阻塞式
WatchKey take(); // 阻塞式
文件属性
NIO.2新增java.nio.file.attribute包用于文件属性的读取和修改。
- XxxAttributeView:文件属性的视图;
- XxxAttributes:文件属性的集合,通过XxxAttributeView对象获取XxxAttributes;
字符集
16位Unicode字符序列与字节序列之间的一个命名的映射,允许以尽可能最具可移植性的方式读写字符序列。
- CharsetDecoder:解码器,将字节数据解码为字符;
- CharsetEncoder:编码器,将字符数据转换回字节;