高性能 IO 的进化历程
1、BIO
locking I/O 指的是 java 传统的 IO 网络编程,当一个请求在处理时,其他的请求都必须等待其处理结束后才能进行接入,这样大大的降低了服务器的处理能力,虽然后期使用线程池来进行优化,但是整体的性能还是受制于整个系统可支持的线程数量。
1.1、单线程
请求之间按序处理,只有前一个请求的连接断开之后下一个请求才能进来处理,只要不释放,其余请求都将会阻塞等待。
public class BIOServerSocket {
public static void main(String[] args) {
ServerSocket serverSocket=null;
try {
serverSocket=new ServerSocket(8080);
System.out.println("启动服务:监听端口:8080");
while(true) {
Socket socket = serverSocket.accept(); //连接阻塞
System.out.println("client connected: " + socket.getPort());
BufferedWriter bufferedWriter = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write("connection sucess " + socket.getPort() + ": \n");
bufferedWriter.flush();
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
String clientStr = bufferedReader.readLine();
System.out.println("receive a message: " + clientStr);
bufferedWriter.write("receive a message:" + clientStr + "\n");
bufferedWriter.flush();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(serverSocket!=null){
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
为了方便连接测试,我们就不编写客户端来连接了,直接使用 telnet 指令来进行连接,观察处理的结果。
当第一个连接没有断开的时候,第二个连接只能阻塞:
第一个连接处理结束之后,第二个连接才能进入进行请求处理:
1.2、线程池
针对于前者只有一个线程进行处理,会阻塞大量的请求的缺陷,后台使用线程池来接受客户端的请求,每进来一个请求就交给一个线程去处理,这样就会提高请求的处理效率。
public class BIOServerSocketWithThread {
static ExecutorService executorService= Executors.newFixedThreadPool(10);
public static void main(String[] args) {
ServerSocket serverSocket=null;
try {
serverSocket=new ServerSocket(8080);
System.out.println("启动服务:监听端口:8080");
while(true) {
Socket socket = serverSocket.accept(); //连接阻塞
System.out.println("client connected: " + socket.getPort());
executorService.submit(new SocketThread(socket));
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(serverSocket!=null){
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
将每个请求封装一个任务来处理,单独线程去处理。
public class SocketThread implements Runnable{
private Socket socket;
public SocketThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(this.socket.getInputStream()));
BufferedWriter bufferedWriter = new BufferedWriter(
new OutputStreamWriter(this.socket.getOutputStream()));
bufferedWriter.write("connection sucess " + socket.getPort() + ": \n");
bufferedWriter.flush();
String clientStr = bufferedReader.readLine();
System.out.println("receive a message: " + clientStr);
bufferedWriter.write("receive a message:" + clientStr + "\n");
bufferedWriter.flush();
}catch (Exception e){
e.printStackTrace();
}finally {
//TODO 关闭 IO 流
}
}
}
这样只要在线程创建的消耗范围内,请求就不会被阻塞在外面。但是这种方式最大的优点也是最大缺点,十分的依赖于服务器所支持的线程数量,所以适合处理少量连接,且长时间连接处理的请求,对于大量的释放快速的连接则显得捉襟见肘。
2、NIO
non-blocking IO 和其名字一样,非阻塞说的是当一个请求进来发现服务器的数据还没有准备好,这个时候将不会在这里进行等待结果,而是直接返回。
这里涉及三个核心角色:
| 角色 | 作用 |
|---|---|
| channel | 通道,相当于一个客户端的连接 |
| buffer | NIO 是面向缓冲的,buffer 提供对数据的存储 |
| selector | 多路复用器,注册 channel 并筛选就绪事件 |
2.1、Channel
传统的 IO 操作是基于流式的,通过 read 、write 来读取数据或者写入数据,是单向传输的,在没有读到数据的时候就会阻塞等待数据的响应,也就是数据的传输是一读一写这样来交互的。如果一个客户端建立连接之后,服务端在等待数据的接收,然而客户端因为网络等问题数据传输较慢,就会导致 IO 长时间的陷入等待。而且这种情况是无法预先进行估计的,也就是说无法避免突如其来的 IO 阻塞,将会浪费大量的资源。
而 channel 是一个双向通道,可以在一个通道上进行双向数据传输,类比就像从单工通信进化到了双工通信一般。数据的接收需要一个 buffer ,写出也需要一个 buffer ,也就是需要一个中间的媒介来存储实际的数据信息,可以在创建通道时指定是否阻塞,设置为阻塞就和传统的流式通道一样了,并且支持异步处理。
常用的 channel 实现:
| 类 | 作用 |
|---|---|
| FileChannel | 建立从文件中读取数据的通道 |
| DatagramChannel | 建立通过 UDP 读取网络数据的通道 |
| SocketChannel | 建立通过 TCP 读取网络数据的通道 |
| ServerSocketChannel | 可以监听新进来的 TCP 连接,并且为每个连接创建一个 SocketChannel 进行服务 |
-
FileChannel 主要使用方法:
常用方法 作用 read(ByteBuffer var1) 将通道中的数据读取到缓冲区当中 write(ByteBuffer var1) 将缓冲区中的数据写入到通道当中 transferTo(long var1, long var3, WritableByteChannel var5) 把当前通道中的数据复制到参数通道中 transferFrom(ReadableByteChannel var1, long var2, long var4) 把参数通道中的数据复制到当前通道中 写出数据到文件中:
public class FileChannelTest { public static void main(String[] args) throws IOException { FileOutputStream outputStream = new FileOutputStream("file.txt"); // 获取文件流通道 FileChannel fileChannel = outputStream.getChannel(); // 建立缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); // 将数据写入缓冲区 buffer.put("hello world".getBytes(StandardCharsets.UTF_8)); // 转换位读取标志 buffer.flip(); // 向通道写入数据 fileChannel.write(buffer); if (outputStream != null) { outputStream.close(); } } }从文件中读取数据:
public class FileChannelTest { public static void main(String[] args) throws IOException { FileInputStream inputStream = new FileInputStream("file.txt"); // 建立通道 FileChannel channel = inputStream.getChannel(); // 创建缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); int len = 0; // 读取文件中的数据, 也可以一次性创建比较大的缓冲区来读取 while (true) { len = channel.read(buffer); if (len == -1) break; // 切换读取数据 buffer.flip(); System.out.println(new String(buffer.array())); buffer.clear(); } if (inputStream != null) { inputStream.close(); } } }文件之间的复制:
public class FileChannelTest { public static void main(String[] args) throws IOException { FileInputStream inputStream = new FileInputStream("file.txt"); FileOutputStream outputStream = new FileOutputStream(new File("copy.txt")); // 建立通道 FileChannel in = inputStream.getChannel(); FileChannel out = outputStream.getChannel(); // 数据复制 out.transferFrom(in, 0, in.size()); if (inputStream != null) { inputStream.close(); } if (outputStream != null) { outputStream.close(); } } } -
SelectableChannel
客户端与服务器连接过程中主要使用 SelectableChannel 的实现作为通道(ServerSocketChannel , SocketChannel),主要使用方法:
| 方法 | 作用 |
|---|---|
| register(Selector sel, int ops) | 将本 channel 注册到一个选择器中,并指定响应的状态码。方法还有一个重载支持放入一个 attachment 进行操作。 |
| configureBlocking(boolean var1) | 设置当前通道的阻塞情况,如果是 true 则和一般的阻塞 IO 没有区别,因此想要使用 NIO 的高效连接必须设置为非阻塞的。 |
- ServerSocketChannel 用在服务器端建通新的客户端的 socket 连接。
- SocketChannel 是网络 IO 通道,负责具体的读写操作,NIO 总是把缓冲区中的数据写入到通道当中,或者是将通道中的数据写入到缓冲区中。
2.2、Buffer
缓冲区本质上是一块存储数据的内存,这块内存被封装成一个 NIO BUFFER 对象,他是一个特殊的数组,对象提供了一系列的方法用以快速的访问该块内存。channel 需要使用缓冲区作为媒介来进行数据访问处理,也就是说其实 channel 不具备数据的保存能力。
缓冲区可以认为是一种数据结构,其定义了几个属性来支撑功能的使用:
| 属性 | 描述 |
|---|---|
| capacity | 缓冲区的总体大小,在缓冲区创建的时候进行指定,不可更改 |
| position | 下一个将要被读写的数据的位置索引,根据实际的处理过程会自动变化 |
| limit | 表示当前缓冲区实际存储数的大小。如果是读状态下就是实际数据的大小,数据位置最多读取到这里;如果是写状态则和容量大小一致,表示只能够向缓冲区中写入这么多的数据。再进行读写切换的时候会转换大小。 |
NIO 提供了很多的缓冲区的实现类,但是我们通常情况下使用最多就是 ByteBuffer 这个类,因为他已经可以满足我们的大多数需求,而且通过字节进行传输的效率也会更加的高。
| 方法 | 作用 |
|---|---|
| allocateDirect(int capacity) | 直接在物理内存中创建一块区域作为缓冲区,空间较大不用经过 JVM 的内存模型读取速度较快,但是不受垃圾收集器的控制,创建和销毁较为消耗性能 |
| allocate(int capacity) | 间接在堆中创建缓冲区,有缺点和直接创建的刚好相反 |
| wrap(byte[] array) | 通过一个字节数组大小来创建缓冲区 |
| get() | 取出一个 byte 数据 |
| put(byte var1) | 放入一个 byte 数据 |
| clear() | 将 position 置为 0 ,limit 置为 capacity。虽然实际的数据未被清除,但是这些数据可以被覆盖掉,相当于文件的覆盖写入模式 |
| compact | 将所有未读的数据移动到 buffer 的起始处,并将 position 指向最后一个未读数据的下一个位置,limit 置为 capacity ,相当于文件的追加写入模式 |
缓冲区的使用一般分为以下几步:
- 创建缓冲区将数据写入;
- 调用
flip切换读写模式; - 从缓冲读取数据;
- 调用
clear、compact清空缓冲区。
2.3、Selector
selector 叫做多路复用选择器,主要用于检查通道是否 IO 准备就绪,并将准备就绪的通道放在一个集合中,等待取出进行数据的操作。也就是说 IO 不用阻塞线程,而是主动通知线程处理,这样就可以由一个线程管理多个网络连接。
选择器管理着一个被注册的通道集合和他们的就绪状态,创建通道之后需要注册到一个选择器中,并且使用选择器来跟踪更新通道的状态。Selector 是一个抽象类,在实际中使用的是他的实现类 SeletorImpl。
| 方法 | 作用 |
|---|---|
| open() | 静态方法用于创建一个 selector |
| select() | 无限等待直到有通道做好了 IO 准备,或者有其他线程进行唤醒 |
| select(long var1) | 指定事件内如果没有结果返回则直接返回 0 |
| selectNow() | 该方法执行就绪检查,如果当前没有通道准备就绪立即返回 0 |
一个通道在注册到选择器中时会绑定一个 SelectionKey 作为关系,根据不同的绑定关系进行不同的 IO 处理流程。SelectionKey 提供四种状态参数 OP_READ = 1 读,OP_WRITE 写, OP_CONNECT 连接,OP_ACCEPT 接受。
-
服务器
public class NIOSelectorServerSocket implements Runnable { Selector selector; ServerSocketChannel serverSocketChannel; public NIOSelectorServerSocket(int port) throws IOException { selector = Selector.open(); serverSocketChannel = ServerSocketChannel.open(); // 如果采用selector模型,必须要设置非阻塞 serverSocketChannel.configureBlocking(false); serverSocketChannel.socket().bind(new InetSocketAddress(port)); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); } @Override public void run() { while (!Thread.interrupted()) { try { // 阻塞等待事件就绪 selector.select(); Set selected = selector.selectedKeys(); Iterator it = selected.iterator(); while (it.hasNext()) { // 分发事件处理 dispatch((SelectionKey) it.next()); // 移除当前就绪的事件, 防止多次处理 it.remove(); } } catch (IOException e) { e.printStackTrace(); } } } private void dispatch(SelectionKey key) throws IOException { if (key.isAcceptable()) { // 连接事件 register(key); } else if (key.isReadable()) { // 读事件 read(key); } else if (key.isWritable()) { // 写事件 } } private void register(SelectionKey key) throws IOException { ServerSocketChannel channel = (ServerSocketChannel) key.channel(); SocketChannel socketChannel = channel.accept(); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); } private void read(SelectionKey key) throws IOException { SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); channel.read(byteBuffer); System.out.println("Server Receive :" + new String(byteBuffer.array())); } public static void main(String[] args) throws IOException { NIOSelectorServerSocket selectorServerSocket = new NIOSelectorServerSocket(8080); new Thread(selectorServerSocket).start(); } }
服务器启动之后就会等待客户端的连接,第一次连接的状态是 OP_ACCEPT ,也就是状态值 16 ,服务器接收之后将会进行注册到选择器中(转换为 ServerSocketChannel),等待客户端的消息。如果是读、写事件,则转换为 SocketChannel 从缓冲区中进行数据的操作即可。读、写事件一般都是成对的,也就是服务器接收到客户端的数据之后,将此事件再注册为写事件到选择器中等待就绪向客户端写入数据。
3、IO 模型
3.1、单 Reactor 单线程
此模型在整个处理流程中都是单线程进行的,在 reactor 拿到就绪的 channel 之后交给对应的类去处理,拿到的通道有可能是 ServerSocketChannel , 也有可能是 SocketChannel ,前者的具体处理只需要将其重新注册到选择器中进行实践等待即可,后者需要对读、写进行操作,在此期间整个服务状态处于阻塞,因为线程为 IO 去服务了。
-
Reactor 负责管理选择器并进行通道就绪事件的检查,并进行注册转发。
public class Reactor implements Runnable{ private final Selector selector; private final ServerSocketChannel serverSocketChannel; public Reactor(int port) throws IOException { selector=Selector.open(); serverSocketChannel= ServerSocketChannel.open(); serverSocketChannel.socket().bind(new InetSocketAddress(port)); serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT,new Acceptor(selector,serverSocketChannel)); } @Override public void run() { while(!Thread.interrupted()){ try { selector.select(); Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while(iterator.hasNext()){ dispatch(iterator.next()); iterator.remove(); } } catch (IOException e) { e.printStackTrace(); } } } private void dispatch(SelectionKey key){ Runnable runnable=(Runnable)key.attachment(); if(runnable!=null){ runnable.run(); } } } -
Acceptor 负责处理连接事件的通道,并进行后续事件的注册。
public class Acceptor implements Runnable{ private final Selector selector; private final ServerSocketChannel serverSocketChannel; public Acceptor(Selector selector, ServerSocketChannel serverSocketChannel) { this.selector = selector; this.serverSocketChannel = serverSocketChannel; } @Override public void run() { SocketChannel channel; try { channel=serverSocketChannel.accept(); System.out.println("收到一个客户端连接: " + channel.getRemoteAddress()); channel.configureBlocking(false); channel.register(selector, SelectionKey.OP_READ,new Handler(channel)); } catch (IOException e) { e.printStackTrace(); } } } -
Handler 负责处理具体的 IO 事件,执行IO 时整个服务器会被阻塞。
public class Handler implements Runnable { SocketChannel channel; public Handler(SocketChannel channel) { this.channel = channel; } @Override public void run() { ByteBuffer buffer = ByteBuffer.allocate(1024); try { channel.read(buffer); System.out.println("Server receive Msg(" + channel.getRemoteAddress() + "): " + new String(buffer.array())); } catch (Exception e) { e.printStackTrace(); } finally { if (channel != null) { try { channel.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
3.2、单 Reactor 多线程
多线程指的是对于 IO 事件处理的多线程,也就是针对于单线程处理 IO 事件的优化,引入线程池来执行 handler,类似于 BIO 情况下的多线程处理,其余的处理细节和前者一致。
-
Handler 多线程处理
public class MutilDispatchHandler implements Runnable { SocketChannel channel; private Executor executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); public MutilDispatchHandler(SocketChannel channel) { this.channel = channel; } @Override public void run() { processor(); } private void processor() { executor.execute(new ReaderHandler(channel)); } static class ReaderHandler implements Runnable { private SocketChannel channel; public ReaderHandler(SocketChannel channel) { this.channel = channel; } @Override public void run() { ByteBuffer buffer = ByteBuffer.allocate(1024); try { channel.read(buffer); System.out.println("Server receive Msg(" + channel.getRemoteAddress() + "): " + new String(buffer.array())); } catch (Exception e) { e.printStackTrace(); } finally { if (channel != null) { try { channel.close(); } catch (IOException e) { e.printStackTrace(); } } } } } }
3.3、主从 Reactor 多线程
上面两种模型都是单一 Reactor 来进行注册转发的,导致 Reactor 的职责太重,可以将 Reactor 分为连接和 IO 处理两个部分,主 Reactor 就负责接收用户的连接进来,然后通过 Acceptor 转发到从 Reactor 中进行后续的 IO 连接处理,同时将从 Reactor 交给线程池去管理,这样的话就能有效的提高主 Reactor 的处理效率。
这一部分因为是异步线程执行的原因,逻辑上可能会比较的绕,我们先从主 Reactor 的职责开始捋思路。初始化的时候 ServerSocketChannel 会绑定一个 Acceptor 对象,然后主 Reactor 在处理已就绪通道时就可以把初始化注册的 Acceptor 取出来执行执行(这个时候处理的是连接状态)。
在 Acceptor 中持有从 Reactor 的列表,在接收到连接之后将当前的通道注册到一个从 Reactor 中(这里采用轮询的方式去选择),并且指定具体的 Handler 去处理。而从 Reactor 是交给线程池去处理的,我们在添加状态绑定之后就可以去唤醒 select 阻塞,然后来将当前的事件真正注册到从 Reactor 的选择器中,等待就绪触发。
这里有两次的注册,第一次是将封装好的 Handler 注册到从 Reactor 的阻塞队列中,并没有直接注册到选择器中。第二次注册才是将阻塞队列中的 Handler 取出来,Handler 中持有当前通道,将这个通道实际注册到选择器中,这样才完成通道的注册。
从 Reactor 在执行时就可以取出注册到各自选择器中的事件,按照对应绑定状态进行处理,这里就是简单的网络 IO 处理了。
-
Reactor
public class Reactor implements Runnable { private final Selector selector; private ConcurrentLinkedQueue<AsyncHandler> events = new ConcurrentLinkedQueue<>(); public Reactor() throws IOException { this.selector = Selector.open(); } public Selector getSelector() { return selector; } @Override public void run() { while (!Thread.interrupted()) { AsyncHandler handler; try { while ((handler = events.poll()) != null) { //可以 handler.getChannel().configureBlocking(false); SelectionKey selectionKey = handler.getChannel().register(selector, SelectionKey.OP_READ); selectionKey.attach(handler); handler.setSelectionKey(selectionKey); } selector.select(); Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); // 主 Reactor 得到 Acceptor实例, 从 Reactor 拿到 Handler 实例 Runnable runnable = (Runnable) key.attachment(); if (runnable != null) { runnable.run(); } iterator.remove(); } } catch (IOException e) { e.printStackTrace(); } } } public void register(AsyncHandler handler) { events.offer(handler); // 这里需要唤醒 Selector 的阻塞,将刚进来的通道真正注册到选择器中 selector.wakeup(); } } -
AsyncHandler
public class AsyncHandler implements Runnable { private SocketChannel channel; private SelectionKey selectionKey; StringBuilder stringBuilder = new StringBuilder(); ByteBuffer inputBuffer = ByteBuffer.allocate(1024); ByteBuffer outputBuffer = ByteBuffer.allocate(1024); public AsyncHandler(SocketChannel channel) { this.channel = channel; } public SocketChannel getChannel() { return channel; } public SelectionKey getSelectionKey() { return selectionKey; } public void setSelectionKey(SelectionKey selectionKey) { this.selectionKey = selectionKey; } @Override public void run() { try { if (selectionKey.isReadable()) { read(); } else if (selectionKey.isWritable()) { write(); } } catch (Exception e) { } } private void read() throws IOException { inputBuffer.clear(); int n = channel.read(inputBuffer); if (inputBufferComplete(n)) { System.out.println(Thread.currentThread().getName() + ": Server端收到客户端的请求消息:" + stringBuilder.toString()); outputBuffer.put(stringBuilder.toString().getBytes(StandardCharsets.UTF_8)); this.selectionKey.interestOps(SelectionKey.OP_WRITE); } } private boolean inputBufferComplete(int bytes) throws EOFException { if (bytes > 0) { inputBuffer.flip(); while (inputBuffer.hasRemaining()) { // 得到输入的字符 byte ch = inputBuffer.get(); // 表示Ctrl+c if (ch == 3) { throw new EOFException(); } else if (ch == '\r' || ch == '\n') { return true; } else { stringBuilder.append((char) ch); } } } else if (bytes == 1) { throw new EOFException(); } return false; } private void write() throws IOException { int write = -1; outputBuffer.flip(); if (outputBuffer.hasRemaining()) { // 把收到的数据写回到客户端 write = channel.write(outputBuffer); } outputBuffer.clear(); stringBuilder.delete(0, stringBuilder.length()); if (write <= 0) { this.selectionKey.channel().close(); } else { channel.write(ByteBuffer.wrap("\r\nreactor> ".getBytes())); // 又转化为读事件 this.selectionKey.interestOps(SelectionKey.OP_READ); } } } -
Acceptor
public class Acceptor implements Runnable { final Selector selector; final ServerSocketChannel serverSocketChannel; private final int POOL_SIZE = Runtime.getRuntime().availableProcessors(); private Executor subReactorExecutor = Executors.newFixedThreadPool(POOL_SIZE); private Reactor[] subReactors = new Reactor[POOL_SIZE]; int handerNext = 0; public Acceptor(Selector selector, int port) throws IOException { this.selector = selector; this.serverSocketChannel = ServerSocketChannel.open(); this.serverSocketChannel.socket().bind(new InetSocketAddress(port)); this.serverSocketChannel.configureBlocking(false); this.serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT, this); init(); System.out.println("Main Reactor Acceptor: Listening on port:" + port); } private void init() throws IOException { for (int i = 0; i < subReactors.length; i++) { subReactors[i] = new Reactor(); subReactorExecutor.execute(subReactors[i]); } } @Override public void run() { try { SocketChannel socketChannel = serverSocketChannel.accept(); //获取连接 if (socketChannel != null) { socketChannel.write(ByteBuffer.wrap("Multiply Reactor Patterm\r\nreactor> ".getBytes())); System.out.println(Thread.currentThread().getName() + ": Main-Reactor-Acceptor:" + socketChannel.getLocalAddress() + "连接"); Reactor subReactor = subReactors[handerNext]; subReactor.register(new AsyncHandler(socketChannel)); if (++handerNext == subReactors.length) { handerNext = 0; } } } catch (IOException e) { e.printStackTrace(); } } }
4、AIO
异步IO 的概念和 同步IO 相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。在一个CPU密集型的应用中,有一些需要处理的数据可能放在磁盘上。预先知道这些数据的位置,所以预先发起异步IO读请求。等到真正需要用到这些数据的时候,再等待异步IO完成。使用了异步IO,在发起IO请求到实际使用数据这段时间内,程序还可以继续做其他事。
虽然看起来异步执行的方式是比较时尚的,但是也比较鸡肋。把磁盘的操作交给异步线程去执行,主线程可以腾出手来做其他的事情,但是主线程也需要等待异步线程的结果返回。如果处理的时间多的话,主线程最终就会一直在这里等待,反而没有降低了效率。
因此现在对于 AIO 的使用并不怎么的广,包括 Netty 框架也有使用 AIO 作为底层模型的想法,但是最终也使用了 NIO 来实现。但随着不断的技术优化,AIO 可能也有冲出重围的一天,这里就不做过多的介绍了。