Reactor模式整理

一些思考

先看下传统IO编程模型:

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
public class ClassicServer {
public static void main(String[] args) {
try{
ServerSocket serverSocket = new ServerSocket(8899);
while(true) {
Socket socket = serverSocket.accept();
// new Thread(new Handler(socket)).start();
new Handler(socket).run();
}
} catch(IOException ex){
ex.printStackTrace();
}
}

static class Handler implements Runnable{ // 每个Handler在单独的线程中执行
final Socket socket;

public Handler(Socket socket) {
this.socket = socket;
}

public void run() {
try{
byte[] input = new byte[Integer.MAX_VALUE];
socket.getInputStream().read(input); // 从流读取
byte[] output = process(input); // 业务处理
socket.getOutputStream().write(output); // 写入到流
} catch(IOException ex){
ex.printStackTrace();
}
}

private byte[] process(byte[] cmd){ return cmd; }
}
}

在学习完NIO后,心中一直有个疑问:不使用多线程的传统IO编程模型要在一个死循环中不断的接受连接然后处理请求,而NIO的编程模型也是要依次遍历监听到的已就绪的IO事件,同样也需要在某个循环中挨个处理,那么NIO相比于传统IO在处理时有什么本质区别吗?

实际上,如果不使用多线程,那么传统IO编程模型每次在调用完accept()后都要同步的去调用socket.getInputStream().read(input);阻塞地从流中读取数据。此时的问题在于,如果客户端在与服务器连接后迟迟不发送请求,那么线程就会一直阻塞在这里,这时候如果有别的客户端发送任何请求过来都没办法及时处理。当然,这个问题可以通过多线程来解决,也就是将上述Handler.run()放在单独的新线程中执行,但是线程的创建和销毁以及切换开销都是非常大的,所以在高并发的场景下性能将急剧下降,这也是NIO的出现所解决的问题之一。

通过使用NIO,accept()后,并不需要阻塞的去读取客户端发送过来的数据,而是将读事件注册到Selector上,等之后读事件就绪时再进行相应的处理即可(这可能发生在很多次的select()方法执行之后了)。因此,NIO使用一个线程就能达到传统IO要使用多个线程才能实现的目的。然而,这里有一点容易被忽视:这种单线程使用NIO的方式只能同步的依次去处理每一个事件,也就是说,在并发量比较低的场景下,使用多线程的传统IO编程模型可能会有更好的性能,因为它利用了多核的优势,这也是在Doug Lea的那篇文章中将Reactor模式不断演进的动力之一。

优质资料