Java IO - BIO
BIO,blocking IO,同步阻塞IO。最容易理解、最容易实现的IO工作方式。
应用程序向操作系统请求网络IO操作,这时应用程序会一直等待;另一方面,操作系统收到请求后,也会等待,直到网络上有数据传到监听端口;操作系统在收集数据后,会把数据发送到应用程序;最后应用程序收到数据,并解除等待状态。
概念
阻塞IO & 非阻塞IO
这两个概念是应用程序级别的。
主要描述的是应用程序请求操作系统IO操作后,如果IO资源没有准备好,那么应用程序该如何处理的问题。
- 阻塞IO:应用程序等待
- 非阻塞IO:应用程序继续执行(并且使用线程一直轮询,直到有IO资源准备好了)
同步IO & 异步IO
这两个概念是操作系统级别的。
主要描述的是操作系统在收到应用程序请求IO操作后,如果IO资源没有准备好,该如何响应应用程序的问题。
- 同步IO:不响应,直到IO资源准备好以后
- 异步IO:返回一个标记(好让应用程序和自己知道之后准备好的数据往哪里通知),当IO资源准备好以后,再用事件机制返回给应用程序。
传统的BIO通信方式简介
以前大多数网络通信方式都是阻塞模式的,即:
- 客户端向服务器端发出请求后,客户端会一直等待(不会再做其他事情),直到服务器端返回结果或者网络出现问题。
- 服务器端同样的,当在处理某个客户端A发来的请求时,另一个客户端B发来的请求会等待,直到服务器端的这个处理线程完成上一个处理。
BIO的问题
- 同一时间,服务器只能接受来自于客户端A的请求信息;虽然客户端A和客户端B的请求是同时进行的,但客户端B发送的请求信息只能等到服务器接受完A的请求数据后,才能被接受。
- 由于服务器一次只能处理一个客户端请求,当处理完成并返回后(或者异常时),才能进行第二次请求的处理。很显然,这样的处理方式在高并发的情况下,是不能采用的。
多线程方式 - 伪异步
上面说的情况是服务器只有一个线程的情况,那么是否可以使用多线程技术来解决这个问题:
- 当服务器收到客户端X的请求后,(读取到所有请求数据后)将这个请求送入一个独立线程进行处理,然后主线程继续接受客户端Y的请求。
- 客户端一侧,也可以使用一个子线程和服务器端进行通信。这样客户端主线程的其他工作就不受影响了,当服务器端有响应信息的时候再由这个子线程通过 监听模式/观察模式(等其他设计模式)通知主线程。
- 虽然在服务器端,请求的处理交给了一个独立线程进行,但是操作系统通知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这边接受信息的情况。
模拟20个客户端并发请求,服务器端使用单线程
多线程来优化服务器端
看看服务器端的执行效果
问题根源
1 | Socket socket = serverSocket.accept(); |
重点的问题并不是“是否使用了多线程”,而是为什么accept()、read()方法会被阻塞。
serverSocket.accept()会被阻塞?
它内部的实现是使用的操作系统级别的同步IO。