Java中的三大IO模型
在JDK1.4之前,基于Java所有的socket通信都采用了同步阻塞模型(BIO),这种模型性能低下,当时大型的服务均采用C或C++开发,因为它们可以直接使用操作系统提供的异步IO或者AIO,使得性能得到大幅提升。
2002年,JDK1.4发布,新增了java.nio包,提供了许多异步IO开发的API和类库。新增的NIO,极大的促进了基于Java的异步非阻塞的发展和应用。
2011年,JDK7发布,将原有的NIO进行了升级,称为NIO2.0,其中也对AIO进行了支持。
BIO模型
java中的BIO是blocking I/O的简称,它是同步阻塞型IO,其相关的类和接口在java.io下。
BIO模型简单来讲,就是服务端为每一个请求都分配一个线程进行处理,如下:
示例代码:
public class BIOServer {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(6666);
ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
System.out.println("等待客户端连接。。。。");
Socket socket = serverSocket.accept(); //阻塞
executorService.execute(() -> {
try {
InputStream inputStream = socket.getInputStream(); //阻塞
byte[] bytes = new byte[1024];
while (true){
int length = inputStream.read(bytes);
if(length == -1){
break;
}
System.out.println(new String(bytes, 0, length, "UTF-8"));
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
}}
这种模式存在的问题:客户端的并发数与后端的线程数成1:1的比例,线程的创建、销毁是非常消耗系统资源的,随着并发量增大,服务端性能将显著下降,甚至会发生线程堆栈溢出等错误。
当连接创建后,如果该线程没有操作时,会进行阻塞操作,这样极大的浪费了服务器资源。
NIO模型
NIO,称之为New IO 或是 non-block IO (非阻塞IO),这两种说法都可以,其实称之为非阻塞IO更恰当一些。
NIO相关的代码都放在了java.nio包下,其三大核心组件:Buffer(缓冲区)、Channel(通道)、Selector(选择器/多路复用器)
Buffer
在NIO中,所有的读写操作都是基于缓冲区完成的,底层是通过数组实现的,常用的缓冲区是ByteBuffer,每一种java基本类型都有对应的缓冲区对象(除了Boolean类型),如:CharBuffer、IntBuffer、LongBuffer等。
Channel
在BIO中是基于Stream实现,而在NIO中是基于通道实现,与流不同的是,通道是双向的,既可以读也可以写。
Selector
Selector是多路复用器,它会不断的轮询注册在其上的Channel,如果某个Channel上发生读或写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey获取就绪Channel的集合,进行IO的读写操作。
基本示意图如下:
可以看出,NIO模型要优于BIO模型,主要是:
通过多路复用器就可以实现一个线程处理多个通道,避免了多线程之间的上下文切换导致系统开销过大。
NIO无需为每一个连接开一个线程处理,并且只有通道真正有有事件时,才进行读写操作,这样大大的减少了系统开销。
示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class SelectorDemo {
/**
* 注册事件
*
* @return
*/
private Selector getSelector() throws Exception {
//获取selector对象
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false); //非阻塞
//获取通道并且绑定端口
ServerSocket socket = serverSocketChannel.socket();
socket.bind(new InetSocketAddress(6677));
//注册感兴趣的事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
return selector;
}
public void listen() throws Exception {
Selector selector = this.getSelector();
while (true) {
selector.select(); //该方法会阻塞,直到至少有一个事件的发生
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
process(selectionKey, selector);
iterator.remove();
}
}
}
private void process(SelectionKey key, Selector selector) throws Exception {
if(key.isAcceptable()){ //新连接请求
ServerSocketChannel server = (ServerSocketChannel)key.channel();
SocketChannel channel = server.accept();
channel.configureBlocking(false); //非阻塞
channel.register(selector, SelectionKey.OP_READ);
}else if(key.isReadable()){ //读数据
SocketChannel channel = (SocketChannel)key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
channel.read(byteBuffer);
System.out.println("form 客户端 " + new String(byteBuffer.array(), 0, byteBuffer.position()));
}
}
public static void main(String[] args) throws Exception {
new SelectorDemo().listen();
}
}
AIO模型
在NIO中,Selector多路复用器在做轮询时,如果没有事件发生,也会进行阻塞,如何能把这个阻塞也优化掉呢?那么AIO就在这样的背景下诞生了。
AIO是asynchronous I/O的简称,是异步IO,该异步IO是需要依赖于操作系统底层的异步IO实现。
AIO的基本流程是:用户线程通过系统调用,告知kernel内核启动某个IO操作,用户线程返回。kernel内核在整个IO操作(包括数据准备、数据复制)完成后,通知用户程序,用户执行后续的业务操作。
kernel的数据准备
将数据从网络物理设备(网卡)读取到内核缓冲区。
kernel的数据复制
将数据从内核缓冲区拷贝到用户程序空间的缓冲区。
目前AIO模型存在的不足:
需要完成事件的注册与传递,这里边需要底层操作系统提供大量的支持,去做大量的工作。
Windows 系统下通过 IOCP 实现了真正的异步 I/O。但是,就目前的业界形式来说,Windows 系统,很少作为百万级以上或者说高并发应用的服务器操作系统来使用。
而在 Linux 系统下,异步IO模型在2.6版本才引入,目前并不完善。所以,这也是在 Linux 下,实现高并发网络编程时都是以 NIO 多路复用模型模式为主。
Reactor模型
Reactor线程模型不是Java专属,也不是Netty专属,它其实是一种并发编程模型,是一种思想,具有指导意义。比如,Netty就是结合了NIO的特点,应用了Reactor线程模型所实现的。
Reactor模型中定义的三种角色:
- Reactor:负责监听和分配事件,将I/O事件分派给对应的Handler。新的事件包含连接建立就绪、读就绪、写就绪等。
- Acceptor:处理客户端新连接,并分派请求到处理器链中。
- Handler:将自身与事件绑定,执行非阻塞读/写任务,完成channel的读入,完成处理业务逻辑后,负责将结果写出channel。
常见的Reactor线程模型有三种,如下:
- Reactor单线程模型
- Reactor多线程模型
- 主从Reactor多线程模型
单Reactor单线程模型
说明:
- Reactor充当多路复用器角色,监听多路连接的请求,由单线程完成
- Reactor收到客户端发来的请求时,如果是新建连接通过Acceptor完成,其他的请求Handler完成。
- Handler完成业务逻辑的处理,基本的流程是:Read –> 业务处理 –> Send 。
这种模型优点:
- 结构简单,由单线程完成,没有多线程、进程通信等问题
- 适合在一些业务逻辑比较简单、对于性能要求不高的应用场景
缺点:
- 由于是单线程操作、不能充分发挥多核CPU的性能
- 当Reactor线程负载过重之后、处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重Reactor线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈。
- 可靠性差,如果该线程进入死循环或意外终止,就会导致整个通信系统不可用,容易造成单点故障。
单Reactor多线程模型
说明:
- 在Reactor多线程模型相比较单线程模型而言,不同点在于,Handler不会处理业务逻辑,只是负责响应用户请求,真正的业务逻辑,在另外的线程中完成。
- 这样可以降低Reactor的性能开销,充分利用CPU资源,从而更专注的做事件分发工作了,提升整个应用的吞吐。
但是这个模型存在的问题:
多线程数据共享和访问比较复杂。如果子线程完成业务处理后,把结果传递给主线程Reactor进行发送,就会涉及共享数据的互斥和保护机制。
Reactor承担所有事件的监听和响应,只在主线程中运行,可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。
为了解决性能问题,产生了第三种主从Reactor多线程模型。
主从Reactor多线程模型
在主从模型中,将Reactor分成2部分:
- MainReactor负责监听server socket,用来处理网络IO连接建立操作,将建立的socketChannel指定注册给SubReactor。
- SubReactor主要完成和建立起来的socket的数据交互和事件业务处理操作。
该模型的优点:
响应快,不必为单个同步事件所阻塞,虽然Reactor本身依然是同步的。
可扩展性强,可以方便地通过增加SubReactor实例个数来充分利用CPU资源。
可复用性高,Reactor模型本身与具体事件处理逻辑无关,具有很高的复用性。
Netty模型
Netty模型是基于Reactor模型实现的,对于以上三种模型都有非常好的支持,也非常的灵活,一般情况,在服务端会采用主从架构模型,基本示意图如下:
说明:
在Netty模型中,负责处理新连接事件的是BossGroup,负责处理其他事件的是WorkGroup。Group就是线程池的概念。
NioEventLoop表示一个不断循环的执行处理任务的线程,用于监听绑定在其上的读/写事件。
通过Pipeline(管道)执行业务逻辑的处理,Pipeline中会有多个ChannelHandler,真正的业务逻辑是在ChannelHandler中完成的。