乐趣区

关于java:后端面经JavaIO多路复用-简录

0. Java 线程 IO 模型

Java 当中的线程 I / O 模型如图所示:

1. BIO

当一个线程进行 I / O 操作的时候,传统的做法是阻塞期待,直到 I / O 操作实现再持续后续的操作,这种 IO 形式就是 BIO(Blocking I/O)。

BIO 形式的毛病是:

  • 大量并发线程的场景下效率过低;
  • 空期待浪费资源;

2. NIO

JDK1.4 引入了 NIO(No Blocking I/ O 或者是 New I/O)。NIO 是一种同步非阻塞的 I / O 模型,绝对于 BIO,NIO 容许一个线程在 I / O 操作的时候解决其余工作,然而须要定期轮询查看 I / O 操作是否实现。
NIO 的毛病在于:

  • 轮询的工夫距离不好把握;
  • 一个线程解决一个 I / O 操作,如果存在大量 I /O,解决其余工作和轮询操作重复切换状态,上下文切换开销大;

3. I/ O 多路复用(次要)

3.1 概念

为了解决 NIO 的毛病,Linux 引入了 I / O 多路复用的机制,即一个线程能够同时监听多个 I / O 操作,当某个 I / O 操作实现后,会告诉线程进行解决。
多路指的是多个 SOCKET 连贯之间的 I / O 操作,复用指的是共用一个线程。
I/ O 多路复用的长处在于:

  • 一个线程能够同时监听多个 I / O 操作,缩小了线程的数量,防止了线程切换的开销;

须要留神的是,I/ O 多路复用只有和 NIO 配合应用能力发挥作用,因为 NIO 是非阻塞的,所以能够在一个线程中同时监听多个 I / O 操作,而 BIO 是阻塞的,一个线程只能解决一个 I / O 操作,所以无奈实现 I / O 多路复用。

3.2 实现

I/ O 多路复用的实现思路:
对于多个 socket 连贯,程序提供一个文件描述符汇合给零碎,当某个接口的 I / O 操作实现后,会告诉线程进行解决。

实现 I / O 多路复用的形式有三种:selectpollepoll

1. select

函数原型如下所示:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • nfds:文件描述符的数量,即文件描述符汇合中最大的文件描述符加 1;
  • readfds:读文件描述符汇合;
  • writefds:写文件描述符汇合;
  • exceptfds:异样文件描述符汇合;
  • timeout:超时工夫;

从参数能够看进去 select 形式监听读、写、异样事件。

select依据监听的事件类型别离创立三个文件描述符数组,而后在 timeout 工夫内阻塞线程进行监听,直到有事件产生或者超时。之后查看数组中是否有事件达到。
select的毛病在于:

  • 文件描述符数组大小无限,为 1024,因而对于高并发场景并不实用;
  • 维持三个文件描述符数组,占据大量的内存空间;
  • 每次调用 select 须要将数组从用户空间拷贝到内核空间,同时从新对数组进行遍历查找,效率低;

2. poll

函数原型如下所示:

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fds:文件描述符数组;
  • ndfs:文件描述符数组的大小;
  • timeout:超时工夫;

实质的工作过程和 select 相似,然而略微做了改良,只须要构建一个数组,并且数组大小不受限制,而是可能自在指定;
poll的毛病在于:

  • 每次调用 poll 之后都须要进行数组遍历,这一点并没有改良

3. epoll

为了解决 selectpoll的毛病,在高并发场景下,不同的操作系统引入了不同的解决方案,例如 Linux 引入了 epoll、FreeBSD 引入了kqueue、Solaris 引入了/dev/poll
epoll实现 I / O 多路复用,步骤如下:

  1. 先创立 epoll 对象:

    int epfd = epoll_create(10);

    其中,int epoll_create(int size)会在内核空间开拓一块指定大小的数据表,并由 epfd 指向这部分内存。

  2. 创立好 epoll 对象之后,应用 epoll_ctl 将注册须要监听的事件:

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  3. epfd是创立数组之后的内存指针;
  4. op是操作类型,包含三种模式:

    • EPOLL_CTL_ADD:增加须要监听的事件;
    • EPOLL_CTL_MOD:批改须要监听的事件;
    • EPOLL_CTL_DEL:删除须要监听的事件;
  5. fd是须要监听的文件描述符,须要反对 NIO;
  6. event记录了注册事件的具体信息。数据结构如下所示:

    typedef union epoll_data {
     void    *ptr;
     int      fd;
     uint32_t u32;
     uint64_t u64;
    } epoll_data_t;
    
    struct epoll_event {
     uint32_t     events;    /* Epoll events */
     epoll_data_t data;      /* User data variable */
    };
  7. 应用 epoll_wait 进行监听:
    epoll_wait函数原型如下所示:

    int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
  8. epfd是创立数组之后的内存指针;
  9. evlist是用于寄存事件的数组, 也是返回的后果数组,蕴含被触发事件的对应文件描述符;

    • 这里显示了和 selectpoll 的区别,selectpoll会返回所有文件描述符而后遍历,而 epoll 只会返回被触发事件的文件描述符;
  10. maxevents是监听事件的最大容量;
  11. timeout是超时工夫;
    监听步骤是 block 的,也就是阻塞的,只有超时才会返回;

epoll 的长处在于:

  • 只返回触发事件的文件描述符,防止了整个数组的遍历;
  • 反对程度触发(Level Trigger)和边缘触发(Edge Trigger)两种模式;

    • 对于程度触发和边缘触发,具体解释可参考这篇博客;

      4. AIO

      AIO(Asynchronous I/O),即异步非阻塞 I / O 模型,AIO 的实现形式是基于事件和回调机制的,当一个 I / O 操作实现后,会告诉线程进行解决,因而不须要轮询操作。

AIO 和 NIO 的区别在于:

  • NIO:线程须要定时查看 I / O 操作是否实现;
  • AIO:安心去做其余事件,等到告诉之后才会进行解决;

5. 技术比照

5.1 BIO、NIO、I/ O 多路复用、AIO 比照

5.2 selectpollepoll比照

6. 面试模仿

Q:IO 多路复用是什么意思?
A:IO 多路复用指的是一个线程治理多个 IO 连贯,监听多个 IO 事件;

Q:NIO 的具体含意
A:NIO 个别了解为 Not Blocking IO,即非阻塞 IO,和传统的 BIO(阻塞 IO)相比,NIO 模型中,一个线程在 IO 操作的时候能够解决其余工作,定期轮询查看 IO 操作是否实现

Q:基于什么实现的 I / O 多路复用?
A:传统的实现形式包含 selectpoll,然而这两类办法都须要遍历数组,效率较低,为此不同的操作系统提出了不同的改良计划,例如 solaris 提出了/dev/poll,FreeBSD 提出了kqueue,Linux 提出了epoll,而epoll 相比于 selectpoll 的次要区别就是返回的事件列表只包含触发事件的文件描述符,而不是全副监听事件的文件描述符,改良了数组遍历这一监听形式。

参考资料

  1. 一文彻底了解 Java IO 模型(阻塞 IO 非阻塞 IO/IO 多路复用)
  2. IO 多路复用机制详解
  3. 讲讲 BIO 和 NIO 以及 IO 多路复用
退出移动版