BIO
说到Java IO流,就会想到OutputStream
(字节输出流)、InputStream
(字节输入流)、Writer
(字符输出流)、Reader
(字符输入流) 这些概念。
IO操作提供了操作 本地文件、网络资源、数据库读写 等功能,这些都是需要和磁盘或者网络打交道的,这类操作叫做耗时操作,容易导致服务阻塞。当然像 ByteArrayInputStream
和 ByteArrayOutputStream
这种只操作内存的IO流除外。
所以BIO叫做 同步阻塞IO,可以通过线程池的方式解决,将每个IO操作单独分配给一个线程,这样主线程就不会被阻塞了。线程池的方式也有效避免了线程重复创建和销毁导致的资源消耗
NIO
JDK1.4以后为我们提供了NIO工具类,在 java.nio
包下
NIO可以在只有一个线程的情况下实现非阻塞操作,主要是通过 Buffer
(缓冲区)、Channel
(通道)、Selector
(选择器) 来实现的。
客户端向服务端发起连接时会先获取到一个SocketChannel,Selector会用一个集合去存储绑定这个Channel的SelectionKey。客户端发数据不会直接将数据实时写入服务端,而是先将数据写入到本地Buffer中,Buffer就起到一个缓冲区的作用,如果缓冲区满了,就向Channel中写入数据,Selector监听到某个SelectionKey有事件发生,就通过这个SelectionKey获取到这个Channel,然后从Channel中读取数据。这样设计的好处就是,Server不需要一直和Client保持连接,而是Client需要Server去处理数据的时候Server再去处理。这样Server就可以同时处理多个IO操作,如果都没有IO请求,Server甚至还可以干自己的事情。
服务端在单线程情况下(也是可以用多线程的,Netty就是基于NIO的,Netty可以创建线程池去处理IO操作)使用一个Selector循环遍历存储SelectionKey的集合,如果某个SelectionKey有事件发生,Selector就处理这个SelectionKey对应的Channel中的数据。
NIO特点是非阻塞、基于事件监听的机制,所以NIO又叫 同步非阻塞IO
AIO
jdk1.7提供支持
简单来说,就叫 异步非阻塞IO,用多线程的方式处理IO。这样一看好像和NIO没啥区别嘛。NIO也是非阻塞的,搞个线程池不就是AIO了?
实际上他们区别大了,上面说NIO是通过Selector遍历所有的Channel去实现事件监听的,这么做其实是 伪事件监听 的做法。
就好比,在医院,5个小伙伴打吊针,他们分别在1~5楼,如果是NIO的方式,只需要一个护士小姐姐从一楼到五楼都走上一遍,看他们的点滴瓶子空了没,这还是个反复的过程。如果是BIO,一个还不够,需要5个小姐姐盯着自己对号的小伙伴。AIO在NIO的基础上更近一层,通常也叫NIO2.0。AIO实现了真正的事件监听,如果哪个小伙伴点滴瓶子空了,不需要小姐姐再过来检查了,而是会自己通知小姐姐过来。中间这段时间,小姐姐就不用再到处跑了,可以安心在自己办公室做个面膜。可以看得出来,AIO比NIO在对系统资源的调度方面更进了一步。
AIO事件通知需要操作系统的配合。
Buffer
在NIO中,Buffer起到了缓冲数据的作用,也是非阻塞实现的关键,Jdk为我们预定义了一些Buffer实现类
可以看到,是一些基本类型的实现。其中ByteBuffer通常可以用来传输音频,视频,图像等格式,CharBuffer可以传文本字符串
以IntBuffer为例
IntBuffer intBuffer = IntBuffer.allocate(5);
这样就构建了一个 Int类型的Buffer对象,大小为5,如果超出了5个,就会抛出 java.nio.BufferOverflowException
详细例子:
public static void main(String[] args) {
IntBuffer intBuffer = IntBuffer.allocate(5);
for (int i = 0; i < intBuffer.capacity(); ++i) {
intBuffer.put(i);
}
// 超出buffer容量,抛出异常 java.nio.BufferOverflowException
// allocate.put(10);
// 读写切换
intBuffer.flip();
while (intBuffer.hasRemaining()) {
System.out.println(intBuffer.get());
}
}
需要注意,Buffer是半双工的,只能在同一时间读取或写入,不能同时读写,如果需要读写切换,请调用 flip 方法
Channel
继承关系如下,例举几个常用的:FileChannel(本地文件读写通道),SocketChannel(网络客户端读写通道),ServerSocketChannel(网络服务端通道)
可以直接通过Jdk原生的IO对象获取,比如 FileInputStream
对象调用 getChannel
方法获取 FileChannel
public static void main(String[] args) {
String path = ChannelDemo.class.getClassLoader().getResource("in.txt").getPath();
try {
FileChannel channel = new FileInputStream(path).getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.limit()));
} catch (IOException e) {
e.printStackTrace();
}
}
读取resources目录的in.txt文件,获取Channel对象。创建一个Buffer,将数据读入Buffer中,然后打印在控制台上
调用Buffer的 flip 方法会将写入流转变成读取流,并将limit赋值为写入流最后的position位置(也就是最后写入字符的位置),不然的话会读取出一堆空字符串。
看flip源码
public final Buffer flip() {
limit = position; // 读取的时候只读到最后写入位置就行了
position = 0; // 读取的时候从第一个位置开始读
mark = -1;
return this;
}
BIO和NIO最主要的区别就是,BIO并没有通道和缓冲区的概念,只是单纯的将流中的数据读取到另一个写入流或者(字节数组/字符串中)
Selector
Selector中有个Set集合,用来存放所有与Channel绑定的SelectionKey
通过不断遍历这个Set,监听每个SelectionKey是否有事件发生
SelectionKey有4中事件类型
// 读事件
public static final int OP_READ = 1;
// 写事件
public static final int OP_WRITE = 4;
// 连接就绪,只有一次
public static final int OP_CONNECT = 8;
// 接收连接,包括OP_READ和OP_WRITE
public static final int OP_ACCEPT = 16;
注意Selector中的 select()
是阻塞的,它会一直等待直到有事件发生。
select(long timeout)
是非阻塞的,如果指定时间内没有接收到事件,则会退出。selectNow()
也是非阻塞的,他不会等待,如果没有事件,就直接退出。