阻塞式I/O应该算是从Java学习以来用的最多的功能了, 无论是文件读写, 网络I/O, HttpServlet的response, 都是靠java.io包下的API完成的. 而java.io包, 便是阻塞式I/O的实现. 在了解Netty之前, 可以先了解为什么不用阻塞式I/O, 阻塞式I/O的缺点是什么, 毕竟没有对比就没有优劣之分.
阻塞式Socket I/O
代码示例
在Java中, 创建一个Socket服务端的代码通常是这样的:
1 |
ServerSocket server = new ServerSocket(8080); |
读取数据通常是这样的:
1 |
byte[] data = byte[1024]; |
分析
系统资源损耗
当ServerSocket.accept()执行后, 线程会陷入阻塞, 直到监听端口接收到socket连接请求. 如果要执行其他任务, 就需要新开线程, 这自然会消耗系统资源. 在量少的时候可能没太多问题. 但是, 如果访问量大了呢. 这就需要进行优化了.
- 将接收到的Socket放入子线程处理.
但这在高访问量的时候并没有什么用, 每个访问开启一个线程, 系统资源占用会非常严重. - 将接收到的Socket封装成任务放入线程池处理.
将任务放入一个队列中排队, 让线程池中可复用的线程来执行任务.这可以缓解一下系统资源占用的问题. 但是这只是解决了线程数量带来的系统开销问题, 还是没有从根本上解决效率问题.
线程阻塞损耗
这里针对上述的方案2讨论.
在这之前, 可以举个现实中的例子来感受一下.
一个服务窗口在办业务, 需要提供一些证件.
有的人不知道, 在窗口前一张一张地从包里拿出来递过去.
有的人早就知道, 手持证件一直在排队.
例子讲完了, 现在来解释一下吧. 这个例子里面, 服务窗口是线程池中的线程, 证件是传输的数据. 前者在窗口前挤牙膏, 后者的明明能马上处理却不得不等待. 于是后面的所有人都被浪费了前者取证件的时间. 如果想提高效率, 那可以让拿证件的人先在旁边拿, 后面准备好证件的人先办业务, 等拿齐了证件再办业务. 这样可以保证任务快速地被处理, 减少队伍中所有任务的等待时间.
为什么要讲这个例子呢? 我们先看InputStream.read()系列函数的描述, 其中有这么一句:
This method blocks until input data is available, …
其实不止read如此, Stream API大部分方法都是会造成阻塞的.
如果流中暂时没有数据, 这个函数会阻塞, 这就相当于是在窗口前面掏证件了. 如果在连接的网络情况不好的情况下, 是有可能出现大量的丢包, 流中长时间接收不到数据的情况的. 这样一阻塞, 有可能后面排队的Socket中的数据已经发送到了, 正等待接收. 这根上面讲的排队例子很相似, 后面的人准备好了材料却还要等. 虽然有线程池, 但线程池也是有线程上限的, 如果线程池中有一个阻塞, 那就是浪费了资源, 因为流中有数据的任务可以保证在read方法上不发生阻塞. 然而线程池是做不到让阻塞的任务中途休息, 让其他线程来执行的, 所以只能让阻塞线程一直阻塞下去, 浪费线程资源.
总结
由上面一些问题可以看出, 网络I/O的效率问题主要就出在网络上. 向外写数据是很简单的事, 因为数据全都在自己的手上, 只要将其写到连接中即可. 但是, 读数据确是有些麻烦的, 主要在它的不稳定上, 由于网络原因, 不一定能及时收到数据, 所以在没有数据时的处理方式是使用的关键. 阻塞式的处理就是在那干等, 算是最差的处理方式了.
近期评论