本文介绍了Java中的四种I/O模型,同步阻塞,同步非阻塞,多路复用,异步阻塞。同时将NIO和BIO进行了对比。
1、同步,异步,阻塞,非阻塞
从内核角度看I/O操作分为两步:用户层API调用;内核层完成系统调用(发起I/O请求),所以,同步,异步针对用户的API调用阻塞,非阻塞针对IO请求;
同步与异步:
简单来说,同步与异步的重点在于当有多个任务和事件发生时,一个任务或事件的执行是否会导致整个流程的暂时等待;同步就是当有多个事件或任务要发生时,这些任务或事件只能逐个进行,一个任务的进行会导致其他任务的暂时等待;异步就是当有多个事件或任务要发生时,可以并发执行多个任务。
阻塞与非阻塞:
简单理解为发出一个请求操作时,当条件不满足时,是否能立即得到返回应答,如果不能立即获得返回,需要等待,那就阻塞了,如果可以立即返回一个信息告知条件不满足,不会一直等待,就是非阻塞了。
2、阻塞IO与非阻塞IO
首先,IO操作主要包括:对硬盘的读写,对socket的读写和对外设的读写。当用户线程发起一个IO请求时(以读操作为例),内核会检查读取的数据是否就绪,对阻塞IO而言,当要读取的数据未就绪时,会一直等待,直到数据准备就绪;对非阻塞IO而言,当读取的数据未就绪时,会返回一个信息告知用户线程当前要读取的数据未就绪,当数据就绪后,将数据拷贝到用户线程,从而完成IO操作。所以,一个完整的IO操作包括两个阶段:
(1)检查数据是否就绪;
(2)将数据从内核空间拷贝到用户空间;
阻塞IO与非阻塞IO的区别就在于第一个阶段。
3、同步IO与异步IO
实际上同步与异步是针对应用程序与内核的交互而言的。同步过程中进程触发IO操作并等待或者轮询的去查看IO操作是否完成。异步过程中进程触发IO操作以后,直接返回,做自己的事情,IO交给内核来处理,完成后内核通知进程IO完成。同步IO与异步IO的关键区别在于在数据拷贝阶段是由用户线程完成还是内核空间完成,异步IO必须要有操作系统的底层支持。
4、Unix提供的I/O模型
在《Unix网络编程》中介绍了五种IO模型:阻塞IO,非阻塞IO,IO多路复用,信号驱动IO,异步IO;
4.1 阻塞I/O
分为两个阶段:
(1)等待数据就绪,网络I/O就是等待远端数据陆续到达,磁盘I/O就是等待磁盘数据从磁盘上读取到内核态中(数据未就绪就会等待,用户线程处于阻塞状态,用户线程交出CPU);
(2)数据拷贝,将内核空间的数据拷贝一份到用户空间中。
阻塞IO的特点在于IO执行的两个阶段(数据检查和数据拷贝阶段)都被BLOCK了。
4.2 非阻塞I/O
非阻塞I/O分为三个阶段:
(1)socket设置为NONBLOCK就是告诉内核,当I/O请求无法完成时,不要将线程设置为sleep状态,而是返回一个错误码(EWOULDBLOCK),这样线程就不会阻塞了;
(2)I/O操作函数会一致测试数据是否准备好,若没有准备好,继续测试,直到准备好;整个I/O请求过程中,虽然用户线程每次发起请求都会立即返回,但为了等到数据,仍要不断轮询,重复请求,浪费大量CPU资源;
(3)数据准备好了,将其从内核空间拷贝到用户空间。
4.3 多路复用I/O
在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。
I/O多路复用会用到select函数和poll函数,这两个函数会使线程阻塞,与阻塞I/O不同的是:I/O多路复用可以同时对多个I/O操作进行阻塞,还可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或者可写,才真正调用I/O函数。有的人会说:我用多线程+阻塞IO的方式也可以实现这种效果?区别在于,这种方式是每个socket对应一个线程,会造成资源的浪费。多路复用IO模型是一个线程管理多个socket,只有socket真正有读写操作发生才会占用资源进行读写操作,所以,多路复用IO比较适用于连接数比较多的情况。
从流程上来看,使用select函数进行I/O请求和同步阻塞模型没有太大的区别,甚至还多了添加监视Channel,以及调用select函数的额外操作,增加了额外工作。但是,使用 select以后最大的优势是用户可以在一个线程内同时处理多个Channel的I/O请求。用户可以注册多个Channel,然后不断地调用select读取被激活的Channel,即可达到在同一个线程内同时处理多个I/O请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
另外多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断地询问socket状态时通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多。多路复用IO模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用IO模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。
4.4 信号驱动I/O
信号驱动IO就是:允许socket进行信号驱动I/O,并安装一个信号处理函数,线程继续运行并不阻塞。当数据准备好时,线程会收到一个SIGIO 信号,可以在信号处理函数中调用I/O操作函数处理数据。
4.5 异步IO
异步IO模型才是最理想的IO模型
调用aio_read 函数,告诉内核描述字,缓冲区指针,缓冲区大小,文件偏移以及通知的方式,然后立即返回。当内核将数据拷贝到缓冲区后,再通知应用程序。所以异步I/O模式下,阶段1和阶段2全部由内核完成,完成不需要用户线程的参与。
在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用IO函数进行具体的读写。这点是和信号驱动IO模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用IO函数进行实际的读写操作;而在异步IO模型中,收到信号表示IO操作已经完成,不需要再在用户线程中调用iO函数进行实际的读写操作。
前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。
举例说明五种I/O模型
事情:演唱会
角色1:主办方 售房处
角色2:小明
角色3:黄牛
角色4:快递员
阻塞I/O:
小明到售票处进行购票,票未到发售时间,在售票处等待3天直到买到票返回
非阻塞I/O:
小明到售票处进行购票,票未到发售时间,小明回家,过3个小时再来问一次,如此反复直到买到票再次回家
I/O复用(poll epoll select)
小明给黄牛(epoll)打电话,帮我留意买张票。票买到后黄牛打电话告诉小明,小明来取票回家,票没有出来之前,小明就直接干自己的事。
信号驱动I/O
小明想看演唱会,直接给售票处打电话说明,有票了打电话通知一声,小明去取票,票没有出来之前,小明就直接干自己的事。
异步I/O
小明想看演唱会,直接给售票处打电话说明,有票了直接快递员给我送一张到家。
5、Java提供的I/O模型
Unix中的五种I/O模型,除信号驱动I/O外,Java对其它四种I/O模型都有所支持。其中Java最早提供的blocking I/O即是阻塞I/O,而NIO即是非阻塞I/O,同时通过NIO实现的Reactor模式即是I/O复用模型的实现,通过AIO实现的Proactor模式即是异步I/O模型的实现。
BIO+多线程编程实现BIO模型(为每个请求创建一个线程)因为单个线程逐个处理请求,同一时间只能处理一个任务,等待I/O的过程会浪费大量CPU资源,故使用多线程对阻塞I/O模型的改进。一个连接建立成功后,创建一个单独的线程处理其I/O操作。
public class BIOServerThread implements Runnable{
private Socket socket;
public BIOServerThread(Socket socket){
this.socket = socket;
}
public void exe(Socket socket) throws IOException {
try{
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
//获取Socket的输出流,用来向客户端发送数据
PrintStream out = new PrintStream(socket.getOutputStream());
//获取Socket的输入流,用来接收从客户端发送过来的数据
BufferedReader buf = new BufferedReader(new InputStreamReader(socket.getInputStream()));
boolean flag = true;
while(flag){
//接收从客户端发送过来的数据
String str = buf.readLine();
System.out.println("服务端接受到的消息:"+str);
if(str == null || "".equals(str)){
flag = false;
}else{
if("exit".equals(str)){
System.out.println("客户端"+socket.getRemoteSocketAddress()+"退出");;
}else{
//将接收到的字符串前面加上echo,发送到对应的客户端
// out.println("echo:" + str);
System.out.println("服务端输入信息:");
String outStr = input.readLine();
out.println(outStr);
}
}
}
out.close();
buf.close();
socket.close();
}catch(Exception e){
e.printStackTrace();
}
}
@Override
public void run() {
try {
exe(socket);
} catch (IOException e) {
e.printStackTrace();
}
}
}
Server类(服务端)
public class BIOServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket();
Socket socket = null;
while(true){
//与端口绑定
socket.bind(new InetSocketAddress(1234));
//接受客户端连接
socket = serverSocket.accept();
System.out.println("与客户端连接成功");
//启动新线程读取客户端发来的消息
new Thread(new BIOServerThread(socket)).start();
//关闭ServerSocket
serverSocket.close();
}
}
}
Client(客户端)
public class BIOClient {
private static Socket socket ;
public static void send(Socket socket) throws IOException {
BufferedReader in = null;
PrintWriter out = null;
Scanner sc = new Scanner(System.in);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(),true);
while (true){
System.out.println("输入发送消息");
String s = sc.nextLine();
out.print(s);
System.out.println("服务端返回"+in.readLine());
if(s.equals("exit")){
System.out.println("服务端退出");
break;
}
}
in.close();
socket.close();
out.close();
}
public static void main(String[] args) throws IOException {
Socket socket = new Socket();
Scanner in = new Scanner(System.in);
BIOClient.send(socket);
}
}