先来熟悉一下阻塞与非阻塞的概念,二者关注的是等待程序调用结果时的状态。
- 阻塞,指程序返回调用结果之前,当前线程会被挂起。
- 非阻塞,程序返回调用结果之前,线程不会被挂起,可以用过轮询或者回调的方法获取返回结果。
BIO
实现
ServerSocket serverSocket = new ServerSocket(9000);
事件处理
Socket accept = serverSocket.accept();
accept()会阻塞,其原理是调用了一个native的方法socketAccept(),该方法对应JVM中的PlainSocketImpl.c中的Java_java_net_PlainSocketImpl_socketAccept()方法,猜测JVM是通过Linux底层的pthread_mutex_lock和pthread_mutex_unlock来实现阻塞与解阻塞。
缺点
accep()是阻塞的,当处理一个客户端请求时,服务端是没有办法再响应其他客户端的请求。如果对每个客户端都开启一个线程异步的处理,但线程的数量是无法预估的,出现C10k问题将会给服务器带来很大的压力。
NIO
实现(不使用Reactor模型)
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9000));
serverSocketChannel.configureBlocking(false);
事件处理
SocketChannel socketChannel = serverSocketChannel.accept();
ServerSocketChannel的accept()方法是非阻塞的,此时可以通过不断的轮询看是否有事件发生,一旦有事件发生,并不会直接处理,而是放到一个list中,统一进行处理。
缺点
这种方式也会出现C10k问题,因为要遍历放了SocketChannel的list,而这个list中并不是每个Channel都有事件发生,所以在遍历时会产生多余的开销。
实现(使用Reactor模型)
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9000));
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
事件处理
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
这里会阻塞等待事件的发生,并且只关心那些有事件发生的Channel。
NIO三大核心组件
- Channel
- Buffer
- Selector
Channel,类似于流,每个Channel对应一个Buffer,Buffer底层是一个数组。
channel会注册到selector上,由selector根据channel读写事件的发生将其交由某个空闲的线程处理。
NIO的channel和buffer都是既可读又可写。