前提:熟练掌握BIO、NIO、AIO的基本概念以及一些常见问题是你准备面试的过程中不可或缺的一部分,另外这些知识点也是你学习Netty的基础。
BIO,NIO,AIO总结
Java中的BIO、NIO和AIO理解为是Java语言对操作系统的各种IO模型的封装。程序员在使用这些API的时候,不需要关心操作系统层面的知识,也不需要根据不同操作系统编写不同代码。只需要使用Java的API就可以了。
在讲BIO、NIO、AIO之前先来回顾一下这样几个概念:同步与异步,阻塞与非阻塞。
同步与异步
- 同步:同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
- 异步:异步就是发起一个调用后,立刻得到被调用者的回应表示已经接受到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件、回调等机制来通知调用者其返回结果。
同步和异步的区别最大在于异步请求,调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。
阻塞与非阻塞
- 阻塞:阻塞就是发起一个请求,调用者一直等待请求结果的返回,也就是当前线程被挂起,无法从事其他任务,只有当条件就绪(结果返回或者抛出异常)才能继续。
- 非阻塞:非阻塞就是发起一个请求,调用者不用一直等着请求结果返回,可以去干其他事情。
举个生活中简单的例子,你妈妈让你烧水,小时候你比较笨啊,在那里傻等着水开(同步阻塞)。等你稍微再长大一点,你知道每次烧水的空隙可以去干点其他事,然后只需要时不时来看看水开了没有(同步非阻塞)。后来,你们家用上了水开了会发出声音的壶,这样你就只需要听到响声后就知道水开了,在这期间你可以随便干自己的事情,等壶发出声音你就可以去倒水了(异步非阻塞)。
1.BIO(Blocking I/O)
同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。
1.1传统BIO
BIO通信(一请求一应答)模型图如下(图源网络,原出处不明):
采用BIO通信模型的服务端,通常由一个独立的Acceptor(接受器)线程负责监听客户端的连接。我们一般通过在while(true)循环中调用accept()方法等待接收客户端的连接的方式监听请求,请求一旦接收到一个连接请求,就可以建立通信套接字,在这个通信套接字上进行读写操作,此时不能在接收其他客户端的连接请求,只能等待同当前的客户端的操作执行完毕,不过可以通过多线程来支持多个客户端连接,如上图所示。
如果要让BIO通信模型能够同时处理多个客户端请求,就必须使用多线程(主要原因是socket.accept()、socket.read()、socket.write()涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,并销毁线程。这就是典型的请求-应答通信模型。我们可以设想一下,如果这个连接不做任何事情的话就会造成不必要的线程开销,不过可以通过线程池机制改善,线程池还可以让线程的创建和回收成本相对较低。使用FixedThreadPool 可以有效的控制线程的最大数量,保证了系统有限资源的控制,实现了N(客户端请求数量):M(处理客户端请求的线程数)的伪异步I/O模型(N可以远远大于M),下面一节“伪异步BIO”中会详细介绍到。
我们在设想一下当客户端并发量增加后这种模型会出现什么问题???
在 Java 虚拟机中,线程是宝贵的资源,线程的创建和销毁成本很高,除此之外,线程的切换成本也是很高的。尤其在 Linux 这样的操作系统中,线程本质上就是一个进程,创建和销毁线程都是重量级的系统函数。如果并发访问量增加会导致线程数急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务。
1.2伪异步IO
为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化一一一后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数N:线程池最大线程数M的比例关系,其中N可以远远大于M.通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。
伪异步IO模型图(图源网络,原出处不明):
采用线程池和任务队列可以实现一种叫做伪异步的 I/O 通信框架,它的模型图如上图所示。当有新的客户端接入时,将客户端的 Socket 封装成一个Task(该任务实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK 的线程池维护一个消息队列和 M 个活跃线程,对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。不过因为它的底层仍然是同步阻塞的BIO模型,因此无法从根本上解决问题。
1.3代码示例
下面代码中演示了BIO通信(一请求一应答)模型。我们会在客户端创建一个线程多次连接服务端并向其发送”当前时间+:hello world”,服务端会为每个客户端请求创建一个线程来处理。代码示例出自闪电侠的博客,原地址如下:https://www.jianshu.com/p/a4e03835921a
客户端
1 | import java.net.Socket; |
服务端
1 | import java.io.IOException; |
1.4总结
在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。