传统 BIO (Blocking I/O)
BIO (Blocking I/O) 是同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。
BIO 通信(一请求一应答)模型图如下:
传统 BIO 通信模型图采用 BIO 通信模型 的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。一般是在 while(true)
循环中,服务端调用 accept()
方法,等待接收客户端的连接的监听请求,服务端一旦接收到一个连接请求,就可以建立通信套接字,并通过在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接客户端的操作执行完成, 不过可以通过多线程来支持多个客户端的连接,如上图所示。
如果要让 BIO 通信模型 可以同时处理多个客户端请求,就必须使用多线程(主要原因是 socket.accept()
、socket.read()
、socket.write()
涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端连接请求之后,为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的 一请求一应答通信模型 。我们可以设想一下,如果这个连接不做任何事情的话,不就是会造成不必要的线程开销,这是可以通过 线程池机制 来改善的,线程池还可以让线程的创建和回收成本相对较低。使用 FixedThreadPool
可以有效控制线程的最大数量,保证了系统有限资源的控制,实现了 N(客户端请求数量):M(处理客户端请求的线程数量)的伪异步 I/O 模型(N 可以远远大于 M)。
再设想一下,当客户端并发访问量增加后,这种模型又会出现什么问题?
在 Java 虚拟机中,线程是宝贵的资源,创建和销毁成本都很高,除此之外,线程的切换成本也是很高的。尤其在 Linux 这样的操作系统中,线程本质上就是一个进程,创建和销毁线程都是重量级的系统函数。如果并发访问量增加,会导致线程数急剧膨胀,可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务。