关于同步/异步,阻塞/非阻塞,Unix IO模型,可以先看这篇文章网络系统 - Unix IO模型
BIO概述
阻塞式IO。也就是说io没有就绪的时候,操作IO当前线程会被阻塞。也就是用户线程需要等待IO线程完成
服务器实现模式为一个连接一个线程,也就是说,客户端每当有一个连接请求的时候,服务器就需要启动一个对应线程进行处理。但是如果这个连接不做任何事情,就会造成不必要的线程开销。这种模型一般适用于连接数目小且固定的架构。
BIO 其实就是 Reactor的 单reactor 单进程/线程模型

BIO的问题
同一时间,服务器只能接受来自于客户端A的请求信息;虽然客户端A和客户端B的请求是同时进行的,但客户端B发送的请求信息只能等到服务器接受完A的请求数据后,才能被接受。
由于服务器一次只能处理一个客户端请求,当处理完成并返回后(或者异常时),才能进行第二次请求的处理。很显然,这样的处理方式在高并发的情况下,是不能采用的。
多线程方式 - 伪异步方式
上面说的情况是服务器只有一个线程的情况,那么我们就能想到使用多线程技术来解决这个问题:
当服务器收到客户端X的请求后,(读取到所有请求数据后)将这个请求送入一个独立线程进行处理,然后主线程继续接受客户端Y的请求。
客户端一侧,也可以使用一个子线程和服务器端进行通信。这样客户端主线程的其他工作就不受影响了,当服务器端有响应信息的时候再由这个子线程通过 监听模式/观察模式(等其他设计模式)通知主线程。
如下图所示:
这种方式其实就是Reactor 的 单reactor 多线程/多进程模型,同样有是有局限性的,因此也就有了后文的NIO方案:
虽然在服务器端,请求的处理交给了一个独立线程进行,但是操作系统通知accept()的方式还是单个的。也就是,实际上是服务器接收到数据报文后的“业务处理过程”可以多线程,但是数据报文的接受还是需要一个一个的来
在linux系统中,可以创建的线程是有限的。可以通过
cat /proc/sys/kernel/threads-max命令查看可以创建的最大线程数。当然这个值是可以更改的,但是线程越多,CPU切换所需的时间也就越长,用来处理真正业务的需求也就越少。创建一个线程是有较大的资源消耗的。JVM创建一个线程的时候,即使这个线程不做任何的工作,JVM都会分配一个堆栈空间。这个空间的大小默认为128K,可以通过-Xss参数进行调整。当然还可以使用ThreadPoolExecutor线程池来缓解线程的创建问题,但是又会造成BlockingQueue积压任务的持续增加,同样消耗了大量资源。
另外,如果应用程序大量使用长连接的话,线程是不会关闭的。这样系统资源的消耗更容易失控。 那么,如果真想单纯使用线程解决阻塞的问题,那么自己都可以算出来您一个服务器节点可以一次接受多大的并发了。看来,单纯使用线程解决这个问题不是最好的办法。
BIO通信方式深入分析
BIO的问题关键不在于是否使用了多线程(包括线程池)处理这次请求,而在于accept()、read()的操作点都是被阻塞。要测试这个问题,也很简单。这里模拟了20个客户端(用20根线程模拟),利用JAVA的同步计数器CountDownLatch,保证这20个客户都初始化完成后然后同时向服务器发送请求,然后观察一下Server这边接受信息的情况。
服务器端使用单线程
客户端代码(SocketClientDaemon),模拟20个客户端连接
客户端代码(SocketClientRequestThread模拟20个请求)
服务器端(SocketServer) 单个线程
经过执行就会发现,服务器一次只能处理一个客户端请求,当处理完成并返回后(或者异常时),才能进行第二次请求的处理。这就是上面提到的BIO存在的问题
优化服务器端为多线程
客户端代码和上文一样,最主要是更改服务器端的代码:
这里与单线程相比,使用了多线程来处理具体的业务。但还是改变不了.accept()只能一个一个阻塞处理 socket的情况
问题根源
那么重点的问题并不是“是否使用了多线程”,而是为什么accept()、read()方法会被阻塞。
API文档中对于 serverSocket.accept() 方法的使用描述:
Listens for a connection to be made to this socket and accepts it. The method blocks until a connection is made. 翻译一下:监听与此套接字的连接并接受它。该方法会一直阻塞,直到建立连接为止。
这主要就涉及到阻塞式同步IO的工作原理:
服务器线程发起一个accept动作,询问操作系统 是否有新的socket套接字信息从端口X发送过来。accept源码如下:
注意,是询问操作系统。也就是说socket套接字的IO模式支持是基于操作系统的,那么自然同步IO/异步IO的支持就是需要操作系统级别的了。如下:
最后调用的accept0十个native方法,就是调用的操作系统级别的accept。因此如果操作系统没有发现有套接字从指定的端口X来,那么操作系统就会等待。这样serverSocket.accept()方法就会一直等待。这就是为什么accept()方法为什么会阻塞: 它内部的实现是使用的操作系统级别的同步IO