2

前面的文章NIO基础知识介绍了Java NIO的一些基本的类及功能说明,Java NIO是用来替换java 传统IO的,NIO的一些新的特性在网络交互方面会更加的明显。

Java 传统IO的弊端

    “基于JVM来实现每个通道的轮询检查通道状态的方法是可行的,但仍然是有问题的,检查每个通道是否就绪是至少需要一次系统调用,执行的代价是非常昂贵的。同时这种检查不是原子的。列表中的每个通道在检查之后状态变成就绪,但需要等到下一次轮询之前JVM是无法感知的。最糟糕的是,JVM除了不断遍历列表之外将别无选择。JVM无法在某通道就绪时直接得到通知。
    这就是为什么监控多个socket连接的传统的java方案是:为每个socket创建一个线程并使线程可以再read()调用中阻塞,直到数据可用。这实际上将每个阻塞在对应socket上的线程当做了socket事件监控器,并将JVM的线程调度当做了事件通知。但是线程的阻塞和JVM的线程调用都是为了这种目的而设计的。当线程数量增长失控时,JVM为了管理这些线程,导致程序性能降低。”--摘自《JAVA NIO》
上述两段话摘自《JAVA NIO》第四章-选择器中,主要阐述了java 传统NIO的基本实现和弊端。通过上述文字我们也知道了,提高传统IO的性能可以从两方面入手:1.减少线程数量 2.实时获取IO事件


I/O模型

传统I/O为了能实时获取I/O事件,所以才会给每个socket连接分配一个线程用于监控socket事件,因为如何获取I/O事件通知是关键。因此调整JVM的I/O模型是提高I/O性能关键。
我们来了解下I/O模型的类型:

  1. 单线程阻塞I/O模型

    只能同时处理一个客户端请求,并且在I/O操作上是阻塞的,服务端线程会一直等待I/O操作完成,不会做其他的事情。服务端读取客户端数据时要等待客户端发送数据并且操作系统内核复制到用户进程中之后才解除阻塞状态;服务端写数据回客户端是要等待用户进程将数据写入内核并发送到客户端后才解除阻塞状态。单线程阻塞I/O模型无法同时处理多个连接,只能串行处理连接。整个运行过程都只有一个线程,服务端系统资源消耗较小,但并发能力低。


  2. 多线程阻塞I/O模型

    利用多线程机制为每个客户端分配一个线程,每个线程执行读取客户端的数据或数据成功写入客户端后才解除阻塞状态。支持多个客户端并发响应,处理能力得到提高。系统资源消耗较大,多线程之间会产生线程切换成本,同时为了实现线程安全引入线程同步机制导成程序结构复杂。


  3. 单线程非阻塞I/O模型

    在调用读写接口后立即返回,而不会进入阻塞状态;基于事件检测机制获取到事件发生,进行对应事件的I/O操作。
    事件检测方式:

    • 应用程序遍历套接字的事件检测

      服务端程序会保存一个socket套接字连接列表,应用层线程对socket套接字列表轮询尝试读取或写入。如果尝试失败,则在下一个循环再继续尝试。查询每个socket套接字都至少需要一次系统调用,而且无法立即获取socket套接字状态。同时应用程序除了要遍历socket套接字列表之外,还需要处理数据的拼接,实际会占用较多的CPU资源图片描述


    • 内核遍历套接字的事件检测

      将遍历socket列表的工作交给操作系统内核,应用层向发去操作系统内核请求读写列表。操作系统内核会遍历所有套接字并生成对应的可读列表readList和可写列表writeList返回给应用程序。应用程序获取到具体socket,对每个socket进行相应的I/O操作。图片描述
      应用程序向内核请求读写列表,内核遍历所有的套接字并生成对应的可读列表readList和可写列表writeList。readList标明了每个套接字是否可读,例如套接字2、套接字3的值为1表示可读,套接字1的值为0表示不可读;同样,writeList表示套接字是否可写。


    • 内核基于事件回调的事件检测

      遍历套接字列表是个效率比较低的方式,无论是在内核层还是在应用层。操作系统是能够获取到I/O事件操作完成的事件,基于回调函数机制和操作系统的I/O操作控制实现事件检测机制。

      1. 基于回调机制的完全套接字可读可写列表

        图片描述用可读列表readList和可写列表writeList标记读写事件,套接字的数量与readList和writeList两个列表的长度一样。readList元素为1表示可读,writeList元素为1表示可写。当客户端发送数据到服务端,内核完成从网卡复制数据调用回调函数将对应套接字readList中的元素置为1;若套接字已经做好写操作准备,内核会将套接字对应writeList中的元素置为1。应用程序发送请求读、写事件列表,内核会把包含readList和writeList的事件列表返回给应用程序,应用程序分别遍历列表,进而记性读写操作。


      2. 基于回调机制的部分套接字事件列表

        如果套接字数量变大,事件列表传输也是不小的开销。可以让应用层告诉内核每个套接字感兴趣的事件,当客户端发送数据过来时,内核完成从网卡复制后即调用回调函数将套接字相关信息封装成可读事件event放到事件列表中;同样,内核发现网卡可写时会将套接字相关信息封装成可写事件event添加到事件列表中,事件列表中只有处于ready状态的那部分套接字对应的事件。图片描述java NIO即是采用这种事件检测机制,在客户端连接大多数处于活跃的情况下,服务端只用一个线程一直循环处理这些连接,很好地利用了I/O操作的阻塞时间,提高了线程执行效率。


      3. 多线程非阻塞I/O模型

        单线程非阻塞I/O已经大大提高了机器的执行效率,在多核机器上为了更好的提高CPU的使用率可以引入多线程。引入reactor线程模型,每个线程处理不同类型的事件连接。


NIO网络通信

java NIO性能较java传统IO有较大提升主要是在网络交互中,通过Selector来管理客户端的连接,应用程序将客户端socket连接注册到Selector对象上。当客户端socket连接有数据准备就绪或连接准备好写操作时,应用程序通过selector获取到对应socket通道(Channel),应用程序通过Channel对socket进行读写操作。
NIO中服务端一个线程可以同时与多个客户端连接进行读写交互;而传统IO则是服务端的一个线程只能同时处理一个客户端,I/O操作时,线程处于阻塞状态。
Selector是SelectableChannel的多路复用选择器,用于监控SelectableChannel的IO状况的。在Selector定义为SelectableChannel定义了四种事件类型:
SelectionKey.OP_READ        读事件
SelectionKey.OP_WRITE       写事件
SelectionKey.OP_CONNECT    接收到客户端的连接事件
SelectionKey.OP_ACCEPT      接收到客户端的请求事件
多个事件之间用“|”连接
当服务端线程注册事件发生时,操作系统内核会通过回调函数将该事件放到读写事件列表中。JVM通过Selector的select方法会获取到已经注册到Selector对象上的事件列表。若没有事件发生select方法会一直阻塞知道有注册的时间发生才会把发生的时间返回给Selector对象。


浪一把
112 声望5 粉丝