Java NIO核心详解-NIO的简介

高性能的Java通信,绝对离不开Java NIO组件,目前主流的技术框架或中间件服务器,都使用了Java NIO组件,譬如Tomcat、Jetty、Netty。

NIO的起源

NIO技术是怎么来的?为啥需要这个技术呢?先给出一份在Java NIO出来之前,服务器端同步阻塞I/O处理(也就是BIO,Blocking I/O)的参考代码:

package com.information;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ConnectionPerThreadWithPool implements Runnable {

    public void run() {
        //线程池
        //注意,生产环境不能这么用,具体请参考《java 高并发核心编程卷 2》
        ExecutorService executor = Executors.newFixedThreadPool(100);
        try {
            //服务器监听 socket
            ServerSocket serverSocket =
                    new ServerSocket(8081);
            //主线程死循环, 等待新连接到来
            while (!Thread.interrupted()) {
                Socket socket = serverSocket.accept();
                //接收一个连接后,为 socket 连接,新建一个专属的处理器对象
                Handler handler = new Handler(socket);
                //创建新线程来 handle
                //或者,使用线程池来处理
                new Thread(handler).start();
            }
        } catch (IOException ex) { /* 处理异常 */ }
    }
    static class Handler implements Runnable {
        final Socket socket;
        Handler(Socket s) {
            socket = s;
        }
        public void run() {
            //死循环处理读写事件
            boolean ioCompleted=false;
            while (!ioCompleted) {
                try {
                    byte[] input = new byte[1024];
                    /* 读取数据 */
                    socket.getInputStream().read(input);
                    // 如果读取到结束标志
                    // ioCompleted= true
                    // socket.close();
                    /* 处理业务逻辑,获取处理结果 */
                    byte[] output = null;
                    /* 写入结果 */
                    socket.getOutputStream().write(output);
                } catch (IOException ex) { /*处理异常*/ }
            }
        }
    }

    public static void main(String[] args) {
        ConnectionPerThreadWithPool pool = new ConnectionPerThreadWithPool();
        pool.run();
    }
}

以上示例代码中,对于每一个新的网络连接,都通过线程池分配给一个专门线程去负责 IO处理。每个线程都独自处理自己负责的socket连接的输入和输出。当然,服务器的监听线程也是独立的,任何的socket连接的输入和输出处理,不会阻塞到后面新socket连接的监听和建立,这样,服务器的吞吐量就得到了提升。早期版本的Tomcat服务器,就是这样实现的。

这是一个经典的每连接每线程的模型——Connection Per Thread模式。这种模型,在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的I/O并且编程模型简单,也不用过多思考系统的过载、限流等问题。此模型往往会结合线程池使用,线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。

不过,这个模型最本质的问题在于,严重依赖于线程。但线程是很”贵”的资源,主要表目前:

  1. 线程的创建和销毁成本很高,线程的创建和销毁都需要通过重量级的系统调用去完成。
  2. 线程本身占用较大内存,像Java的线程的栈内存,一般至少分配512K~1M的空间,如果系统中的线程数过千,整个JVM的内存将被耗用1G。
  1. 线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。过多的线程频繁切换带来的后果是,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统CPU sy值特别高(超过20%以上)的情况,导致系统几乎陷入不可用的状态。

Java NIO核心详解-NIO的简介

这里关注的是输出信息的第三行,其中:0.3%us表明用户进程所占的百分比;0.3%sy表明内核线程处理所占的百分比;0.0%ni表明被nice命令改变优先级的任务所占的百分比;

99.3%id表明CPU空闲时间所占的百分比;0.0%wa表明等待IO所占的百分比;0.0%hi表明硬件中断所占的百分比,0.0%si表明为软件中断所占的百分比。

当 CPU sy 值高时,表明系统调用耗费了较多的 CPU,对于 Java 应用程序而言,造成这种现象的主要缘由是启动的线程比较多,并且这些线程多数都处于不断的等待(例如锁等待状态)和执行状态的变化过程中,这就导致了操作系统要不断的调度这些线程,切换执行。

  1. 容易造成锯齿状的系统负载。由于系统负载(System Load)是用活动线程数和等待线程数来综合计算的,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求同时到来,从而激活大量阻塞线程从而使系统负载压力过大。

但是,高并发的需求却越来越普通,随着移动端应用的兴起和各种网络游戏的盛行,百万级长连接日趋普遍,此时,必然需要一种更高效的I/O处理组件——这就是Java 的NIO编程组件。

Java NIO简介

在1.4版本之前,Java IO类库是阻塞式IO;从1.4版本开始,引进了新的异步IO库,被称为Java New IO类库,简称为Java NIO。Java NIO类库的目标,就是要让Java支持非阻塞IO,基于这个缘由,更多的人喜爱称JavaNIO为非阻塞IO(Non-Block IO),称“老的”阻塞式Java IO为OIO(Old IO)。总体上说,NIO弥补了原来面向流的OIO同步阻塞的不足,它为标准Java代码提供了高速的、面向缓冲区的IO。

Java NIO类库包含以下三个核心组件:

  • Channel(通道)
  • Buffer(缓冲区)
  • Selector(选择器)

Java NIO,属于第三种模型—— IO 多路复用模型。只不过,Java NIO组件提供了统一的应用开发API,为大家屏蔽了底层的操作系统的差异。后面的章节,我们会对以上的三个Java NIO的核心组件,展开详细介绍。先来看看Java的NIO和OIO的简单对比。

NIO和OIO的对比

在Java中,NIO和OIO的区别,主要体目前三个方面:

(1)OIO是面向流(Stream Oriented)的,NIO是面向缓冲区(Buffer Oriented)的。问题是:什么是面向流,什么是面向缓冲区呢?

在面向流的OIO操作中,IO的 read() 操作总是以流式的方式顺序地从一个流(Stream)中读取一个或多个字节,因此,我们不能随意地改变读取指针的位置,也不能前后移动流中的数据。

而NIO中引入了Channel(通道)和Buffer(缓冲区)的概念。面向缓冲区的读取和写入,都是与Buffer进行交互。用户程序只需要从通道中读取数据到缓冲区中,或将数据从缓冲区中写入到通道中。NIO不像OIO那样是顺序操作,可以随意地读取Buffer中任意位置的数据,可以随意修改Buffer中任意位置的数据。

(2)OIO的操作是阻塞的,而NIO的操作是非阻塞的。

OIO的操作是阻塞的,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。例如,我们调用一个read方法读取一个文件的内容,那么调用read的线程会被阻塞住,直到read操作完成。

NIO如何做到非阻塞的呢?当我们调用read方法时,系统底层已经把数据准备好了,应用程序只需要从通道把数据复制到Buffer(缓冲区)就行;如果没有数据,当前线程可以去干别的事情,不需要进行阻塞等待。NIO的非阻塞是如何做到的呢?实则在上一章,答案已经揭晓了,根本缘由是:NIO使用了通道和通道的IO多路复用技术。

(3)OIO没有选择器(Selector)概念,而NIO有选择器的概念。

NIO技术的实现,是基于底层的IO多路复用技术实现的,列如在Windows中需要select多路复用组件的支持,在Linux系统中需要select/poll/epoll多路复用组件的支持。所以NIO的需要底层操作系统提供支持。而OIO不需要用到选择器。

通道(Channel)

Channel的角色和OIO中的Stream(流)是差不多的。在OIO中,同一个网络连接会关联到两个流:一个输入流(Input Stream),另一个输出流(Output Stream),Java应用程序通过这两个流,不断地进行输入和输出的操作。

在NIO中,一个网络连接使用一个Channel(通道)表明,所有的NIO的IO操作都是通过连接通道完成的。一个通道类似于OIO中的两个流的结合体,既可以从通道读取数据,也可以向通道写入数据。

Java NIO核心详解-NIO的简介

Channel和Stream的一个显著的不同是:Stream是单向的,譬如InputStream是单向的只读流,OutputStream是单向的只写流;而Channel是双向的,既可以用来进行读操作,又可以用来进行写操作。

NIO中的Channel的主要实现有:

  1. FileChannel 用于文件IO操作
  2. DatagramChannel 用于UDP的IO操作
  3. SocketChannel 用于TCP的传输操作
  4. ServerSocketChannel 用于TCP连接监听操作

选择器(Selector)

第一,回顾一个前面介绍的基础知识,什么是IO多路复用模型?

IO多路复用指的是一个进程/线程可以同时监视多个文件描述符(含socket连接),一旦其中的一个或者多个文件描述符可读或者可写,该监听进程/线程能够进行IO事件的查询。

在Java应用层面,如何实现对多个文件描述符的监视呢?

需要用到一个超级重大的Java NIO组件——Selector 选择器。Selector 选择器可以理解为一个IO事件的监听与查询器。通过选择器,一个线程可以查询多个通道的IO事件的就绪状态。

在介绍Selector选择器之前,第一介绍一下这个前置的概念:IO事件。

什么是IO事件呢?表明通道某种IO操作已经就绪、或者说已经做好了准备。例如,如果一个新Channel链接建立成功了,就会在Server Socket Channel上发生一个IO事件,代表一个新连接一个准备好,这个IO事件叫做“接收就绪”事件。再例如,一个Channel通道如果有数据可读,就会发生一个IO事件,代表该连接数据已经准备好,这个IO事件叫做 “读就绪”事件。

Java NIO将NIO事件进行了简化,只定义了四个事件,这四种事件用SelectionKey的四个常量来表明:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

在大家了解了IO事件之后,再回头来看Selector选择器。Selector的本质,就是去查询这

些IO就绪事件,所以,它的名称就叫做 Selector查询者。

从编程实现维度来说,IO多路复用编程的第一步,是把通道注册到选择器中,第二步则是通过选择器所提供的事件查询(select)方法,这些注册的通道是否有已经就绪的IO事件(例如可读、可写、网络连接完成等)。

由于一个选择器只需要一个线程进行监控,所以,我们可以很简单地使用一个线程,通过选择器去管理多个连接通道。

Java NIO核心详解-NIO的简介

与OIO相比,NIO使用选择器的最大优势:系统开销小,系统不必为每一个网络连接(文件描述符)创建进程/线程,从而大大减小了系统的开销。

总之,通过Java NIO可以达到一个线程负责多个连接通道的IO处理,这是超级高效的。这种高效,恰恰就来自于Java的选择器组件Selector以及其底层的操作系统IO多路复用技术的支持。

缓冲区(Buffer)

应用程序与通道(Channel)主要的交互,主要是进行数据的read读取和write写入。为了完成NIO的非阻塞读写操作,NIO为大家准备了第三个重大的组件——NIO Buffer(NIO 缓冲区)。

Buffer顾名思义:缓冲区,实际上是一个容器,一个连续数组。Channel提供从文件、网络读取数据的渠道,但是读写的数据都必须经过Buffer。

Java NIO核心详解-NIO的简介

所谓通道的读取,就是将数据从通道读取到缓冲区中;所谓通道的写入,就是将数据从缓冲区中写入到通道中。缓冲区的使用,是面向流进行读写操作的OIO所没有的,也是NIO非阻塞的重大前提和基础一。

© 版权声明

相关文章

1 条评论

您必须登录才能参与评论!
立即登录
  • 头像
    煜甜甜daytoy 投稿者

    收藏了,感谢分享

    无记录