NIO简介

NIO的诞生

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

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

    static ExecutorService executorService = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(8888);
        Socket socket = null;
        while(true){
            socket = serverSocket.accept();
            handle(socket);
        }

    }

    private static void handle(Socket socket) {
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    byte[] bytes = new byte[1024];
                    socket.getInputStream().read(bytes);
                    System.out.println(new String(bytes));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }

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

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

注:CPU 利用率为 CPU 在用户进程、内核、中断处理、IO 等待以及空闲时间五个部分使用百 
分比。人们往往通过五个部分的各种组合,用来分析 CPU 消耗情况的关键指标。CPU sy 值表 
示内核线程处理所占的百分比。

使用linux 的top命令去查看当前系统的资源,会输出下面的一些指标:

NIO简介_第1张图片

  • 4.2 us表示用户进程所占的百分比 
  • 0.6 sy表示内核线程处理所占的百分比
  • 0.0 ni表示被nice命令改变优先级的任务所占的百分比
  • 88.0 id表示CPU空闲时间所占的百分比
  • 7.0 wa表示等待IO所占的百分比
  • 0.0 hi表示硬件中断所占的百分比
  • 0.1 si表示为软件中断所占的百分比

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

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

注:系统负载(System Load),指当前正在被 CPU 执行和等待被 CPU 执行的进程数目总和,是反映系统忙闲程度的重要指标。当 load 值低于 CPU 数目时,表示 CPU 有空闲,资源存在浪费;当 load 值高于 CPU 数目时,表示进程在排队等待 CPU,表示系统资源不足,影响应用程序的执行性能。       

        因此,当面对十万甚至百万级连接的时候,传统的BIO模型是无能为力的。

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

NIO简介

        在1.4版本之前,Java IO类库是阻塞式IO;从1.4版本开始,引进了新的异步IO库,被称为Java New IO类库,简称为Java NIO。

        Java NIO类库的目标,就是要让Java支持非阻塞IO,基于这个原因,更多的人喜欢称Java NIO为非阻塞IO(Non-Block IO),称“老的”阻塞式Java IO为OIO(Old IO)。总体上说,NIO弥补了原来面向流的OIO同步阻塞的不足,它为标准Java代码提供了高速的、面向缓冲区的IO。

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

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

NIO和OIO的对比

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

1、OIO是面向流(Stream Oriented)的,NIO是面向缓冲区(Buffer Oriented)的。

  •  面向流的OIO操作:IO的   read() 操作总是以流式的方式顺序地从一个流(Stream)中读取一个或多个字节,因此,我们不能随意地改变读取指针的位置,也不能前后移动流中的数据。
  • 面向缓冲区的NIO操作:NIO中引入了Channel(通道)和Buffer(缓冲区)的概念。面向缓冲区的读取和写入,都是与Buffer进行交互。用户程序只需要从通道中读取数据到缓冲区中,或将数据从缓冲区中写入到通道中。NIO不像OIO那样是顺序操作,可以随意地读取Buffer中任意位置的数据,可以随意修改Buffer中任意位置的数据。

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

  • OIO操作:当一个线程调用read()  或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。例如,我们调用一个read方法读取一个文件的内容,那么调用read的线程会被阻塞住,直到read操作完成。
  • NIO操作:当调用read方法时,系统底层已经把数据准备好了,应用程序只需要从通道把数据复制到Buffer(缓冲区)就行;如果没有数据,当前线程可以去干别的事情,不需要进行阻塞等待。【因为NIO使用了通道和通道的IO多路复用技术】

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

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

通道(Channel)

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

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

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

NIO中的Channel的主要实现有:

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

Channel的本质

要Channel的本质,还得回到TCP/IP协议的四层模型的基础知识。具体如下图所示。

NIO简介_第2张图片

        在TCP/IP协议四层模型的最底层为链路层。在最原始的物理链路时代,咱们数据传输的两头(发送方和接收方)会通过拉同轴电缆的方式,拉一条物理电缆(类似于后来更加高级的网线),这条网线就代表一个双向的连接(connection),通过这条电缆,双方可以完成数据的传输。数据传输一旦完成,需要把这条物理链路拆除(就是这么粗暴)。

而在操作系统的维度,该怎么标识这种底层的物理链路的,下面我们来看看:

操作系统一切都是文件描述符(file descriptor)。所以,这种底层的物理链路,在操作系统层面,就会为应用创建一个文件描述符(file descriptor)。

这点和Java里边的对象类似,一个Java对象有内存的数据结构和内存地址,那么,一个文件描述符(file descriptor)也有一个内核的数据结构和一个进程内的唯一编号来表示。然后,操作系统会把这个文件描述提供给应用层,应用层通过对这个文件描述符(file descriptor)去对传输链路进行数据的读取和写入。

注:这里要把文件描述符和文件两个概念,稍加区分。文件这个概念,狭义的理解,就是磁盘上的文件。实际上,Linux 上的文件描述符,除了对磁盘文件做引用之外,还可以引用非磁盘文件。

NIO中的TCP传输通道,实际上就是对底层的传输链路所对应的文件描述符(file descriptor)的一种封装,具体的代码如下:

class SocketChannelImpl extends SocketChannel implements SelChImpl {
    private static NativeDispatcher nd;
    private final FileDescriptor fd;//文件描述符
    .....
}
public final class FileDescriptor {

    private int fd;//文件描述符 的 进程内的唯一编号

    private Closeable parent;
    private List otherParents;
    private boolean closed;
    .....
}

如果两个Java应用通过NIO建立双向的连接(传输链路),它们各自都会有一个自己内部的文件描述符(file descriptor),代表这条连接的自己一方,如下图所示:

NIO简介_第3张图片

选择器(Selector)

1、IO多路复用模型

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

2、监视对多个文件描述符

Java NIO组件【Selector  选择器】是一个IO事件的监听与查询器。通过选择器,一个线程可以查询多个通道的IO事件的就绪状态。

3、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

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

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

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

NIO简介_第4张图片

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

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

 缓冲区(Buffer)

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

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

NIO简介_第5张图片

通道的读取,就是将数据从通道读取到缓冲区中;

通道的写入,就是将数据从缓冲区中写入到通道中 ;

缓冲区的使用,是面向流进行读写操作的OIO所没有的,也是NIO非阻塞的重要前提和基础之一。

        

你可能感兴趣的:(高并发,nio,服务器,java)