前言
根据IO模型,可以把IO分为下面三种
- BIO
BIO(Blocking IO)是最传统的一种IO模型,BIO在读/写数据如果没有可以读取/写入时会发生阻塞。BIO 读写是面向流的,一次性只能从流中读取一个或者多个字节,并且读完之后流无法再读取,需要我们自己讲数据缓存起来,BIO位于java.io
包中。
- NIO
NIO(New IO)采用非阻塞模式,它是基于Reactor模式,IO调用不会被阻塞,它是NIO位于java.nio
包中。
- AIO
AIO 也就是 NIO 2,在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞 的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是说 AIO 模式不需要selector 操作,而是是事件驱动形式,也就是当客户端发送数据之后,会主动通知服务器,接着服务器再进行读写操作。AIO位于java.nio
包中。
BIO、NIO、AIO对比
BIO | NIO | AIO | |
---|---|---|---|
IO模型 | 同步阻塞 | 同步非阻塞(多路复用) | 异步非阻塞 |
编程难度 | 简单 | 复杂 | 复杂 |
可靠性 | 差 | 好 | 好 |
吞吐量 | 低 | 高 | 高 |
JDK版本 | 1.0 | 1.4 | 1.7 |
NIO概述
Java NIO由以下几个核心部分组成
- Channels
- Buffers
- Selectors
Channel
Channel可以翻译成"通道",一个Channel表示一个对象的连接,例如文件,网络socket或者执行不同IO操作的程序。Channel只能是打开或者关闭状态,一旦Channel关闭了就不能再次打开。
它可以对应到BIO中的Stream(流),Channel与Stream的区别是Stream是单向的,Channel是双向的,我们在使用流的时候有InputStream
,OutputStream
,而Channel是双向的,它即可以用来进行读操作,页可以进行写操作。
Channel是一个接口,它提供了如下两个方法
public interface Channel extends Closeable {
// channel是否是打开状态
public boolean isOpen();
// 关闭channel
public void close() throws IOException;
}
复制代码
Channel接口与它的主要实现类的类图如下所示,从图中可以看出Channel的主要实现类有FileChannel
,DatagramChannel
,SocketChannel
,ServerSocketChannel
,分别对应的是文件IO、UDP,TCP Client,TCP Server。
Buffer
Buffer可以翻译成"缓冲区",Buffer是一个基于原始数据类型的线性的、有限元素序列容器,它的本质是一块可以写入数据,然后可以从中读取数据的内存,这块内存被包装成Buffer对象,并提供了一组方法,用来方便的操作该内存。Buffer的主要作用是与Channel进行交互,读取数据时,数据是从Channel读入Buffer,写入数据时,数据是从Buffer写入Channel。
Buffer属性介绍
Buffer是一个抽象类,它位于java.nio
包内,它包含如下属性
// 缓冲区的位置
private int position = 0;
// 缓冲区的限制
private int limit;
// 缓冲区的标记
private int mark = -1;
// 缓冲区的容量
private int capacity;
复制代码
1. position属性
它是要读取或写入的下一个元素的索引。在buffer进行读写模式改变时,position的值会进行相应调整
- 写模式
在刚进入写模式时,position的值为0,表示当前写入位置为从头开始,每当一个数据写入到缓冲区之后,position会向后移动到下一个可写位置,当position的值达到limit时,缓冲区就已经无空间可写了。
- 读模式
当缓冲区刚进入读模式时,position会被重置为0,每读一个数据,position会向后移动到下一个可读为止,当position的值达到limit时,缓冲区就已经无数据可读了。
2. limit属性
可以写入或者读取的数据最大上限
- 写模式
limit属性值的含义为可以写入数据的最大上限,在刚进入写模式时,limit的值会别设置成Buffer的capacity值,表示一直可以将缓冲区内容写满
- 读模式
limit值的含义为最多能从buffer读取数据的最大上限,flip方法将缓冲区切换到读模式时,limit的值会被设置为写模式的position。
3. mark属性
在缓冲区操作过程中,可以将当前position的值临时存入mark属性中,需要的时候再将mark中的属性值取出,暂存的mark值,恢复到position属性中
4. capacity属性
capacity是缓冲区包含的元素数,一旦写入对象数量超过了capacity,缓冲区就满了,不能再写入
Buffer中上述变量大小存在如下关系
0 <= mark <= position <= limit <= capacity
Buffer在读模式与写模式下4个属性的关系如下图所示,以及在两种模式下的可读范围与可写范围
Buffer的创建
如果想要获得一个Buffer对象,可以通过如下方法分配一个Buffer
// 在堆空间中创建一个1024字节的ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 在直接内存中创建一个1024字节的ByteBuffer
ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(1024);
// ByteBuffer直接wrap一个bytes
ByteBuffer wrapBuffer = ByteBuffer.wrap("Hello 林师傅!".getBytes());
// 分配一个1024字符的CharBuffer
CharBuffer charBuffer = CharBuffer.allocate(1024);
复制代码
ByteBuffer中allocate与allocateDirect的区别
想要知道allocate与allocateDirect的区别,我们直接来看两者的源码
// ByteBuffer#allocate
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw createCapacityException(capacity);
return new HeapByteBuffer(capacity, capacity);
}
// ByteBuffer#allocateDirect
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
复制代码
allocate()返回的是HeapByteBuffer对象,它对应的是JVM堆空间的内存。allocateDirect()返回的是DirectByteBuffer,它对应的是直接内存,不受JVM管理。
向Buffer中写数据
写数据到Buffer有两种方式
- 通过Buffer#put()方法写数据
buffer.put(123)
复制代码
- 从Channel写数据到Buffer
channel.read(buffer)
复制代码
从Buffer中读数据
从Buffer中读数据有两种方式
- 通过Buffer#get()方法读数据
// 从byteBuffer获取一个字节
byte b = byteBuffer.get();
// 从byteBuffer中获取一个int
int intResult = byteBuffer.getInt();
// 从byteBuffer将数据写入到数组中,如果数组的长度大于Buffer中可读数据的长度,会抛出异常
byte[] dst = new byte[5];
byteBuffer.get(dst);
复制代码
- 从Buffer读数据到Channel
channel.write(buffer)
复制代码
Buffer其他常用的方法
flip()
:翻转
调用flip()可以将Buffer从写模式切换为读模式,切换之后可以读取之前写入Buffer的数据。抵用flip()会将limit设为position,然后将position设为0,mark设为-1。
// java.nio.Buffer#flip
public Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
复制代码
clear()
:清空
flip()是将Buffer切换为读模式,clear()就是将Buffer切换为写模式,切换之后Buffer可以从头开始写入数据,调用clear()会将position被设为0,limit被设为capacity,mark为-1
// java.nio.Buffer#clear
public Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
复制代码
rewind()
:重新读取
Buffer中的数据读取完之后,调用rewind()方法,可以再次读取数据,它保持limit不变并将position设置为0,mark设为-1。
public Buffer rewind() {
position = 0;
mark = -1;
return this;
}
复制代码
slice()
创建当前Buffer的浅拷贝切片,新创建的Buffer与原Buffer共享一块内存区域,只是将原Buffer从position到limit之间的数据交给新的Buffer引用。我们可以通过下面例子来理解slice()
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println("---put---");
for (int i = 0; i < 8; i++) {
buffer.put((byte) i);
}
printBuf(buffer);
buffer.flip();
for (int i = 0; i < 5; i++) {
buffer.get();
}
System.out.println("---slice---");
printBuf(buffer);
ByteBuffer sliceBuf = buffer.slice();
printBuf(sliceBuf);
}
private static void printBuf(ByteBuffer buffer) {
StringBuilder builder = new StringBuilder();
builder.append("position:").append(buffer.position()).append("; limit:").append(buffer.limit())
.append("; capacity:").append(buffer.capacity()).append(" Content:[");
for (int i = 0; i < buffer.array().length; i++) {
if (i != 0)
builder.append(",");
builder.append(buffer.array()[i]);
}
builder.append("]");
builder.append("\tcontentHashCode:").append(Arrays.hashCode(buffer.array()));
System.out.println(builder);
}
复制代码
输出结果如下,可以看到slice之后获得的新Buffer的position会变成0,limit和capacity的值为limit-position,新Buffer中保存的还是原Buffer数组的引用。
duplicate()
创建缓冲区的浅拷贝,新创建的Buffer与原Buffer共享一块内存区域,新Buffer与原Buffer的mark,position,limit,capacity都相同。我们通过下面例子看理解duplicate()
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10) ;
System.out.println("---put---");
for (int i = 0; i < 10; i++) {
buffer.put((byte) i);
}
printBuf(buffer);
System.out.println("---duplicate---");
ByteBuffer dupBuf = buffer.duplicate(); // dup Buffer
printBuf(dupBuf);
System.out.println("---modify---");
buffer.put(5, (byte) 'a');
printBuf(buffer); // 打印修改后的buf
printBuf(dupBuf); // 打印修改后的dupBuf
}
// ... 省略printBuf代码,与sliceDemo中相同
复制代码
输出结果如下所示,我们可以看到复制后的buffer与原buffer的内容,参数都完全相同,底层共用了一个同一个byte数组(数组hash值相同),修改原buffer后,duplicate出来的buffer也被修改了,这里也可以验证前面说的duplicate只是一个浅拷贝。
Buffer的主要实现类
Buffer中的主要实现类如下所示
Selector
Selector是SelectableChannel的多路复用器,也可以称为选择器,一般用于用于检查一个或多个 NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个 channels,也就是可以管理多个网络链接。使用 Selector 的好处在于: 使用更少的线程来就可以来处理通道了,相比使用多个线程,避免了线程上下文切换带来的开销。
Selector的类图如下,可以看到Selector的最终实现类是KQueueSelectorImpl
继承了SelectorImpl
,在不同的操作系统中,Selector的最终实现类略有不同,在window系统中是WindowsSelectorImpl
,在Linux中是EPollSelectorImpl
,由于笔者用的是macOS,selector的最终实现类是KQueueSelectorImpl
。
Selector的创建
要获得一个Selector对象,可以通过调用Selector.open()方法创建一个Selector对象
Selector open = Selector.open();
复制代码
打开open()的源码,可以发现是通过默认的SelectorProvider来创建的Selector,SelectorProvider是一个单例
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
复制代码
注册Channel到Selector
SelectableChannel中注册方法,将channel注册到selector,并传入感兴趣的SelectionKey
selectableChannel.register(selector,SelectionKey)
复制代码
注意:
- 与Selector配合使用时,Channel必须处于非阻塞模式下
- 一个通道并不一定支持所有SelectionKey事件
使用Selector的好处在于:使用更少的线程就可以来处理通道,相比使用多个线程,避免了线程上下文带来的切换
轮询就绪操作
通过Selector的select()方法,可以查询出已经就绪的通道操作。
// Selector中的
// 阻塞到至少有一个通道在注册上的时间就绪了
public abstract int select() throws IOException;
// 和select()一样,但最长阻塞事件为timeout毫秒
public abstract int select(long timeout) throws IOException;
// 非阻塞,只要通道就绪就立即返回
public abstract int selectNow() throws IOException;
复制代码
SelectionKey详解
SelectionKey表示SelectableChannel在Selector中注册的标识,Chanel向Selector注册时,都会创建一个SelectionKey。SelectionKey保存了注册Channel、注册的Selector、通道事件类型操作符等信息。
SelectionKey是一个抽象类,它有两个实现类AbstractSelectionKey(抽象类)和SelectionKeyImpl(最终实现类),类图如下所示。
SelectionKey包含如下4种操作类型。
// 读操作符,值为1
public static final int OP_READ = 1 << 0;
// 写操作符,值为4
public static final int OP_WRITE = 1 << 2;
// 连接操作符,值为8
public static final int OP_CONNECT = 1 << 3;
// 接收操作符,值为16
public static final int OP_ACCEPT = 1 << 4;
复制代码
SelectableChannel
SelectableChannel是一个可通过Selector多路复用的Channel。我们来看下SelectableChannel的定义,SelectableChannel是一个抽象类,并且实现了Channel方法,SelectableChannel也是DatagramChannel,SocketChannel,ServerSocketChannel的父类,SelectableChannel并不是FileChannel的父类,因此FileChannel不能被选择器复用,SelectableChannel类的源码如下
public abstract class SelectableChannel extends AbstractInterruptibleChannel implements Channel{
// ...
// 获取通道向给定Selector注册的密钥。
public abstract SelectionKey keyFor(Selector sel);
// 向给定Slelector注册此Channel,并返回SelectionKey
public abstract SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException;
// 向给定Slelector注册此Channel,并返回SelectionKey
public final SelectionKey register(Selector sel, int ops) throws ClosedChannelException{return register(sel, ops, null);}
// ...
}
复制代码
如果SelectableChannel要和Selector一起使用,可以通过上面的register方法将当前Channel注册到Selector中,并返回个新的 SelectionKey 对象,该对象表示Channel在Selector中注册的令牌
SelectableChannel至多只能在在某个Selector中注册一次,一旦Channel向Selector注册后,Channel将变成"已注册"状态。
SelectableChannel是线程安全的