NIO
IO多路复用
- selector(基于轮询); JDK的IO复用使用,支持FD_SIZE = 1024
- poll(基于轮询),对selector修改没有上限
- epoll(基于信号量的)
- Kqueue:mac
IO
也叫BIO,阻塞IO.
- JDK支持
import java.io.*; 采用的设计模式是 装饰者(Decorator) - 基于流的数据类型做分类
- 字符流:Reader,Writer;java使用unicode编码,一个字符2个字节
- 字节流:InputStream, OutputStream
内核进程/用户进程切换:
内核将数据复制到用户进程空间(内核进程/用户进程切换:内核进程占用CPU并读取数据到用户进程空间,用户进程停止阻塞不再获取CPU)
NIO
同步非阻塞
- 三大组件:
Buffer,Channel,Selector; 下文有关于三大组件的使用介绍 - Channel + Buffer: 解决了非阻塞问题(这个说法有待商榷...错的,应该是selector,channel + buffer提供了高效的IO)
- Selector是同步的,实现单线程管理多个channel,替换了多线程模式;解决了阻塞的问题
- JDK支持:
java.nio.*; 应用netty - selector管理Channel的IO事件,
- FileChannel 不支持非阻塞。。。。(还需要继续调研)
// tomcat配置NIO支持: 线程池模型 => selector + buffer + 线程池(处理业务)
Connector 的 protocol="org.apache.coyote.http11.Http11NioProtocol"
AIO
异步IO
- 异步通知(需要系统的信号量支持)
- 增加了
Future和回调函数,例如Future/Promise,CompletableFuture;CompletionHandler
总共有三个类需要我们关注,分别是 AsynchronousSocketChannel,AsynchronousServerSocketChannel 和 AsynchronousFileChannel,只不过是在之前介绍的 FileChannel、SocketChannel 和 ServerSocketChannel 的类名上加了个前缀 Asynchronous
思维导图&反思
只有反思过,见到/听到的东西才是自己的东西
IO与NIO区别
- IO阻塞, NIO非阻塞
- IO基于流(字节流 + 字符流); NIO基于通道和缓冲区(channel + buffer + Selector)
2.阻塞-非阻塞和同步-异步
- 阻塞/非阻塞:
针对IO来说的, 指当前进程是否等待IO完成;通过selector实现, - 同步/异步:
针对进程来说,指进程内的多个子任务是否串行执行;通过多线程实现;如果需要返回值,可以通过回调/异步查询的方式。
3.解释内核进程与用户进程的切换
IO为例
技术文档
Buffer
Buffer是在机器内存上分配的空间,不在JVM的空间中;这段空间的使用类似C程序的共享内存(C语言共享内存实现进程的通信一样);特别注意的是内存的溢出和内存的释放
简单的类图: Buffer <-继承 ByteBuffer <-继承 MappedByteBuffer ; 尬...Buffer是最上层的abstract类,ByteBuffer是重要的实现,MappedByteBuffer是内存映射文件(在ROcketMQ源码中有使用); ByteBuffer同等级的实现还有IntBuffer,CharBuffer.....
// Buffer的源码
public abstract class Buffer {
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
// 当前读/写指针的位置
private int position = 0;
// 写模式下,可写的空间
private int limit;
// 内存空间的大小
private int capacity;
/*
* buf.put(magic); // 写 header
* in.read(buf); // 从channel 读书数据到buf
* buf.flip(); // 切换模式
* out.write(buf); // 从buf 读书到 out
* buffer从写模式 切换到 读模式
*/
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
// 使用mark标记pos当前位置
public final Buffer mark() {
mark = position;
return this;
}
// 恢复pos到上一次mark的位置
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
// 重置pos
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
// 清空指针
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
}
jdk8引入的直接内存,和堆内存空间, 跟netty的分配很像
Channel
统一封装用户IO操作,便于selector管理,完成了BIO中流(stream)的作用。
// 这里需要从面向对象的角度去读
FileChannel channel = file.getChannel();
ByteBuffer buf = ByteBuffer.allocate(10);
channel.read(buf); // read channel into buf
buf.flip();
channel.write(buf); // write channel from buf
FileChannel 不支持非阻塞。。。这个是在其他博文看到的,还没有测试
Selector
selector实现单线程管理多个channel,通过循环校验代替了阻塞;
// Selector的范式
public static void testSelector() {
try {
// TCP Server
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
// 默认=true是阻塞的
// 只能注册非阻塞的事件
serverSocketChannel.configureBlocking(false);
// 获取Selector
Selector selector = Selector.open();
// 注册监听时间
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 判断是否有事件准备好
int readyChannels = selector.select();
if (readyChannels == 0) {
continue;
}
// 遍历处理;此处使用的循环校验; 不是阻塞
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
// 处理准备好的事件
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
CompletionHandler
回调函数
public interface CompletionHandler<V,A> {
// A attchment是个透传参数
void completed(V result, A attachment);
void failed(Throwable exc, A attachment);
}
AsynchronousFileChannel
异步文件IO
首先,我们就来关注异步的文件 IO,前面我们说了,文件 IO 在所有的操作系统中都不支持非阻塞模式,但是我们可以对文件 IO 采用异步的方式来提高性能,下面,我会介绍 AsynchronousFileChannel 里面的一些重要的接口,都很简单,读者要是觉得无趣,直接滑到下一个标题就可以了。
实例化:
AsynchronousFileChannel channel = AsynchronousFileChannel.open(Paths.get("/Users/hongjie/test.txt"));
一旦实例化完成,我们就可以着手准备将数据读入到 Buffer 中:
ByteBuffer buffer = ByteBuffer.allocate(1024);
Future<Integer> result = channel.read(buffer, 0);
异步文件通道的读操作和写操作都需要提供一个文件的开始位置,文件开始位置为 0
除了使用返回 Future 实例的方式,也可以采用回调函数进行操作,接口如下:
public abstract <A> void read(ByteBuffer dst,
long position,
A attachment,
CompletionHandler<Integer,? super A> handler);
顺便也贴一下写操作的两个版本的接口:
public abstract Future<Integer> write(ByteBuffer src, long position);
public abstract <A> void write(ByteBuffer src,
long position,
A attachment,
CompletionHandler<Integer,? super A> handler);
我们可以看到,AIO 的读写主要也还是与 Buffer 打交道,这个与 NIO 是一脉相承的。 另外,还提供了用于将内存中的数据刷入到磁盘的方法:
public abstract void force(boolean metaData) throws IOException;
因为我们对文件的写操作,操作系统并不会直接针对文件操作,系统会缓存,然后周期性地刷入到磁盘。如果希望将数据及时写入到磁盘中,以免断电引发部分数据丢失,可以调用此方法。参数如果设置为 true,意味着同时也将文件属性信息更新到磁盘。
还有,还提供了对文件的锁定功能,我们可以锁定文件的部分数据,这样可以进行排他性的操作。
public abstract Future<FileLock> lock(long position, long size, boolean shared);
position 是要锁定内容的开始位置,size 指示了要锁定的区域大小,shared 指示需要的是共享锁还是排他锁
当然,也可以使用回调函数的版本:
public abstract <A> void lock(long position,
long size,
boolean shared,
A attachment,
CompletionHandler<FileLock,? super A> handler);
文件锁定功能上还提供了 tryLock 方法,此方法会快速返回结果:
public abstract FileLock tryLock(long position, long size, boolean shared)
throws IOException;
这个方法很简单,就是尝试去获取锁,如果该区域已被其他线程或其他应用锁住,那么立刻返回 null,否则返回 FileLock 对象。
AsynchronousServerSocketChannel
AsynchronousSocketChannel
AIO socket
AsynchronousChannelGroup
AIO的线程池模型
// 源码