0%

Java中的IO模型

学Netty之前,得先了解Unix的5种IO模型,Java的3种IO模型;本文主要介绍Java中的3种IO模型(BIO,NIO,AIO);

  • BIO的优化版(线程池实现异步)适用于1000以内的并发场景;
  • NIO多路复用适用于高并发网络场景(Netty基于NIO实现);
  • AIO暂时用的较少;

Java中的IO模型

Java 中的 BIO、NIO和 AIO 理解为是 Java 语言对操作系统的各种 IO 模型的封装。程序员在使用这些 API 的时候,不需要关心操作系统层面的知识,也不需要根据不同操作系统编写不同的代码。只需要使用Java的API就可以了。

在讲 BIO,NIO,AIO 之前先来回顾一下这样几个概念:同步与异步,阻塞与非阻塞。

同步与异步

关于同步和异步的概念解读困扰着很多程序员,大部分的解读都会带有自己的一点偏见。
参考StackoverflowAsynchronous vs synchronous execution, what does it really mean?

When you execute something synchronously, you wait for it to finish before moving on to another task.
当你同步执行某项任务时,你需要等待其完成才能继续执行其他任务。
When you execute something asynchronously, you can move on to another task before it finishes.
当你异步执行某些操作时,你可以在完成另一个任务之前继续进行。

  • 同步:两个同步任务相互依赖,并且一个任务必须以依赖于另一任务的某种方式执行。
    • 比如在A->B事件模型中,你需要先完成 A 才能执行B。 再换句话说,同步调用种被调用者未处理完请求之前,调用不返回,调用者会一直等待结果的返回。
  • 异步: 两个异步的任务完全独立的,一方的执行不需要等待另外一方的执行。再换句话说,异步调用中,调用后就返回结果不需要等待处理结果返回,当处理结果返回的时候通过回调函数或者其他方式拿着结果再做相关事情;

阻塞与非阻塞

  • 阻塞: 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
  • 非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。

如何区分同步/异步阻塞/非阻塞

  • 同步/异步是从行为角度描述事物的;
  • 阻塞和非阻塞描述的当前事物的状态(等待调用结果时的状态)

BIO (Blocking I/O)

BIO
BIO通信(一请求一应答)模型:Each handler may be started in its own thread

采用 BIO 通信模型 的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接。
我们一般通过在while(true)循环中服务端会调用accept()方法等待接收客户端的连接的方式监听请求,请求一旦接收到一个连接请求,就可以建立通信套接字(Scoket),在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成,不过可以通过多线程来支持多个客户端的连接,如上图所示。

BIO如何实现并发?

利用多线程实现并发处理
如果要让 BIO 通信模型能够同时处理多个客户端请求,就必须使用多线程(主要原因是socket.accept()、socket.read()、socket.write() 涉及的三个主要函数都是同步阻塞的),
也就是说它在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。
这就是典型的一请求一应答通信模型 。

线程的使用成本高
在 Java 虚拟机中,线程是宝贵的资源,线程的创建、切换、销毁成本很高。 尤其在 Linux 这样的操作系统中,线程本质上就是一个进程,创建和销毁线程都是重量级的系统函数。 如果并发访问量增加会导致线程数急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死`,不能对外提供服务。

利用线程池实现并发控制
为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化,
后端通过一个线程池来处理多个客户端的请求接入,通过线程池可以灵活地调配线程资源,让线程的创建和回收成本相对较低,可设置线程的最大值,防止由于海量并发接入导致线程耗尽。

如使用FixedThreadPool可以有效的控制了线程的最大数量,保证了系统有限的资源的控制,

实现了N(客户端请求数量):M(处理客户端请求的线程数量)的伪异步I/O模型(N 可以远远大于 M);

采用线程池和任务队列可以实现一种叫做伪异步的 I/O 通信框架
BIO

  • 当有新的客户端接入时,将客户端的 Socket 封装成一个Task(该任务实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK 的线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理。
  • 由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。

伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。
不过因为它的底层仍然是同步阻塞的BIO模型,因此无法从根本上解决问题。

应用场景

伪异步I/O通信框架适用于并发数小于1000的情况。

在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。
但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

NIO (New I/O)

NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。
NIO

  • NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法;
  • NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式;
  • 阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反;
  • 对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;
  • 对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发;

NIO的问题

为什么大家都不愿意用 JDK 原生 NIO 进行开发呢?从上面的代码中大家都可以看出来,是真的难用!除了编程复杂、编程模型难之外,它还有以下让人诟病的问题:

  • JDK 的 NIO 底层由 epoll 实现,该实现饱受诟病的空轮询 bug会导致 cpu 飙升 100%;

  • 项目庞大之后,自行实现的 NIO 很容易出现各类 bug,维护成本较高

    Netty 的出现很大程度上改善了 JDK 原生 NIO 所存在的一些让人难以忍受的问题。

AIO (Asynchronous I/O)

aio
在 JDK1.7中引入了 NIO 的改进版 NIO2,也就是AIO(异步IO),它是异步非阻塞的IO模型。
异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

  • 虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的;
  • 除了 AIO 其他的 IO 类型都是同步的;
  • 目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。

参考