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多路复用的形式有三种:select
、poll
、epoll
。
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
为了解决select
和poll
的毛病,在高并发场景下,不同的操作系统引入了不同的解决方案,例如Linux引入了epoll
、FreeBSD引入了kqueue
、Solaris引入了/dev/poll
。
由epoll
实现I/O多路复用,步骤如下:
先创立epoll对象:
int epfd = epoll_create(10);
其中,
int epoll_create(int size)
会在内核空间开拓一块指定大小的数据表,并由epfd
指向这部分内存。创立好epoll对象之后,应用
epoll_ctl
将注册须要监听的事件:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd
是创立数组之后的内存指针;op
是操作类型,包含三种模式:EPOLL_CTL_ADD
:增加须要监听的事件;EPOLL_CTL_MOD
:批改须要监听的事件;EPOLL_CTL_DEL
:删除须要监听的事件;
fd
是须要监听的文件描述符,须要反对NIO;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 */};
应用
epoll_wait
进行监听:epoll_wait
函数原型如下所示:int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
epfd
是创立数组之后的内存指针;evlist
是用于寄存事件的数组,也是返回的后果数组,蕴含被触发事件的对应文件描述符;- 这里显示了和
select
、poll
的区别,select
、poll
会返回所有文件描述符而后遍历,而epoll
只会返回被触发事件的文件描述符;
- 这里显示了和
maxevents
是监听事件的最大容量;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 select
、poll
、epoll
比照
6. 面试模仿
Q:IO多路复用是什么意思?
A:IO多路复用指的是一个线程治理多个IO连贯,监听多个IO事件;Q:NIO的具体含意
A:NIO个别了解为Not Blocking IO,即非阻塞IO,和传统的BIO(阻塞IO)相比,NIO模型中,一个线程在IO操作的时候能够解决其余工作,定期轮询查看IO操作是否实现Q:基于什么实现的I/O多路复用?
A:传统的实现形式包含select
、poll
,然而这两类办法都须要遍历数组,效率较低,为此不同的操作系统提出了不同的改良计划,例如solaris提出了/dev/poll
,FreeBSD提出了kqueue
,Linux提出了epoll
,而epoll
相比于select
、poll
的次要区别就是返回的事件列表只包含触发事件的文件描述符,而不是全副监听事件的文件描述符,改良了数组遍历这一监听形式。
参考资料
- 一文彻底了解Java IO模型(阻塞IO非阻塞IO/IO多路复用)
- IO多路复用机制详解
- 讲讲BIO和NIO以及IO多路复用