十一 BIO NIO
- 首先,需要澄清一些基本概念:
-
区分同步或异步(synchronous/asynchronous)。简单来说,同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步;而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系。
-
区分阻塞与非阻塞(blocking/non-blocking)。在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如 ServerSocket 新连接建立完毕,或数据读取、写入操作完成;而非阻塞则是不管 IO 操作是否结束,直接返回,相应操作在后台继续处理。
- BIO
IO 都是同步阻塞模式,所以需要多线程以实现多任务处理
public class BioServer extends Thread {
private ServerSocket serverSocket;
private ExecutorService executor;
public int getPort() {
return serverSocket.getLocalPort();
}
public void run() {
try {
executor = Executors.newFixedThreadPool(8); //初始化一个线程池来执行
serverSocket = new ServerSocket(0);
while (true) {
Socket socket = serverSocket.accept();
RequestHandler requestHandler = new RequestHandler(socket);
executor.execute(requestHandler);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
// 简化实现,不做读取,直接发送字符串
class RequestHandler extends Thread {
private Socket socket;
RequestHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (PrintWriter out = new PrintWriter(socket.getOutputStream())) {
out.println("Hello world!");
out.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
DemoServer server = new DemoServer();
server.start();
try (Socket client = new Socket(InetAddress.getLocalHost(), server.getPort())) {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
bufferedReader.lines().forEach(s -> System.out.println(s));
}
}
}
- NIO1
利用了单线程轮询事件的机制,通过高效地定位就绪的 Channel,来决定做什么,仅仅 select 阶段是阻塞的,可以有效避免大量客户端连接时,频繁线程切换带来的问题,应用的扩展能力有了非常大的提高
public class NioServer extends Thread {
public void run() {
try (Selector selector = Selector.open();
ServerSocketChannel serverSocket = ServerSocketChannel.open()) {// 创建 Selector 和 Channel
serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 8888));
serverSocket.configureBlocking(false);
// 注册到 Selector,并说明关注点
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();// 阻塞等待就绪的 Channel,这是关键点之一 当有 Channel 发生接入请求,就会被唤醒。
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
// 生产系统中一般会额外进行就绪状态检查
sayHelloWorld((ServerSocketChannel) key.channel());
iter.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void sayHelloWorld(ServerSocketChannel server) throws IOException {
try (SocketChannel client = server.accept();) {
//通过 SocketChannel 和 Buffer 进行数据操作 发送一段字符串
client.write(Charset.defaultCharset().encode("Hello world!"));
}
}
// 省略了与前面类似的 main
}
-
NIO2 (java7引入, 或叫AIO)
之后介绍 -
由于nio实际上是同步非阻塞io,是一个线程在同步的进行事件处理,当一组事channel处理完毕以后,去检查有没有又可以处理的channel。这也就是同步+非阻塞。同步,指每个准备好的channel处理是依次进行的,非阻塞,是指线程不会傻傻的等待读。只有当channel准备好后,才会进行。
那么就会有这样一个问题,当每个channel所进行的都是耗时操作时,由于是同步操作,就会积压很多channel任务,从而完成影响。那么就需要对nio进行类似负载均衡的操作,如用线程池去进行管理读写,将channel分给其他的线程去执行,这样既充分利用了每一个线程,又不至于都堆积在一个线程中,等待执行.
B和N通常是针对数据是否就绪的处理方式来区别
sync和async是对阻塞进行更深一层次的阐释,区别在于数据拷贝由用户线程完成还是内核完成,讨论范围一定是两个线程及以上了。
同步阻塞,从数据是否准备就绪到数据拷贝都是由用户线程完成 bio
同步非阻塞,数据是否准备就绪由内核判断,数据拷贝还是用户线程完成 nio
异步非阻塞,数据是否准备就绪到数据拷贝都是内核来完成 aio
所以真正的异步IO一定是非阻塞的。
多路复用IO即使有Reactor通知用户线程也是同步IO范畴,因为数据拷贝期间仍然是用户线程完成。
详细可参考 https://tech.meituan.com/nio.html?utm_source=tool.lu