如果有人再问你 Java IO,把这篇文章砸他头上( 五 )


客户端,运行结果如下:

如果有人再问你 Java IO,把这篇文章砸他头上

文章插图
 
本例中测试的客户端数量是 30,服务端使用 java 线程池来处理任务,线程数量为 5 个,服务端不用为每个客户端都创建一个线程,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机 。
在活动连接数不是特别高的情况下,这种模型是还不错,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题 。
但是,它的底层仍然是同步阻塞的 BIO 模型,当面对十万甚至百万级连接的时候,传统的 BIO 模型真的是无能为力的,我们需要一种更高效的 I/O 处理模型来应对更高的并发量 。
6.4.3、NIONIO 中的 N 可以理解为 Non-blocking,一种同步非阻塞的 I/O 模型,在 Java 1.4 中引入,对应的在java.nio包下 。
NIO 新增了 Channel、Selector、Buffer 等抽象概念,支持面向缓冲、基于通道的 I/O 操作方法 。
NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现 。
NIO 这两种通道都支持阻塞和非阻塞两种模式 。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反 。
对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发效率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发 。
我们先看一下 NIO 涉及到的核心关联类图,如下:
如果有人再问你 Java IO,把这篇文章砸他头上

文章插图
 
上图中有三个关键类:Channel 、Selector 和 Buffer,它们是 NIO 中的核心概念 。
  • Channel:可以理解为通道;
  • Selector:可以理解为选择器;
  • Buffer:可以理解为数据缓冲流;
我们还是用前面的城市交通工具来继续形容 NIO 的工作方式,这里的 Channel 要比 Socket更加具体,它可以比作为某种具体的交通工具,如汽车或是高铁、飞机等,而 Selector 可以比作为一个车站的车辆运行调度系统,它将负责监控每辆车的当前运行状态:是已经出站还是在路上等等,也就是说它可以轮询每个 Channel 的状态 。
还有一个 Buffer 类,你可以将它看作为 IO 中 Stream,但是它比 IO 中的 Stream 更加具体化,我们可以将它比作为车上的座位,Channel 如果是汽车的话,那么 Buffer 就是汽车上的座位,Channel 如果是高铁上,那么 Buffer 就是高铁上的座位,它始终是一个具体的概念,这一点与 Stream 不同 。
Socket 中的 Stream 只能代表是一个座位,至于是什么座位由你自己去想象,也就是说你在上车之前并不知道这个车上是否还有没有座位,也不知道上的是什么车,因为你并不能选择,这些信息都已经被封装在了运输工具(Socket)里面了 。
NIO 引入了 Channel、Buffer 和 Selector 就是想把 IO 传输过程中涉及到的信息具体化,让程序员有机会去控制它们 。
当我们进行传统的网络 IO 操作时,比如调用 write() 往 Socket 中的 SendQ 队列写数据时,当一次写的数据超过 SendQ 长度时,操作系统会按照 SendQ 的长度进行分割的,这个过程中需要将用户空间数据和内核地址空间进行切换,而这个切换不是程序员可以控制的,由底层操作系统来帮我们处理 。
而在 Buffer 中,我们可以控制 Buffer 的 capacity(容量),并且是否扩容以及如何扩容都可以控制 。
理解了这些概念后我们看一下,实际上它们是如何工作的呢?
还是以上面的操作为例子,为了方便观看结果,本次的客户端线程请求数改成 15 个 。
客户端,程序如下:
如果有人再问你 Java IO,把这篇文章砸他头上

文章插图
 
服务端,程序如下:
如果有人再问你 Java IO,把这篇文章砸他头上

文章插图
 
先启动服务端程序,再启动客户端程序,看看运行结果!
服务端,运行结果如下:
如果有人再问你 Java IO,把这篇文章砸他头上

文章插图
 
客户端,运行结果如下:
如果有人再问你 Java IO,把这篇文章砸他头上

文章插图
 


推荐阅读