概念:
同步、异步、阻塞、非阻塞的概念
同步:所谓同步,发起一个功能调用的时候,在没有得到结果之前,该调用不返回,也就是必须一件事一件事的做,等前一件做完了,才能做下一件。
提交请求->等待服务器处理->处理完毕返回 这个期间客户端浏览器不能干任何事
异步:调用发出后,调用者不能立刻得到结果,而是实际处理这个调用的函数完成之后,通过状态、通知和回调来通知调用者。
比如ajax:请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕
(在服务器处理的时候,客户端还可以干其他的事)
阻塞:指调用结果返回之前,当前线程会被挂起(CPU不给线程分配时间片),函数只能在得到结果之后才会返回。
(阻塞调用和同步调用的区别)同步调用的时候,当前线程仍然可能是激活的,只是在逻辑上当前函数没有返回。例如:在Socket中调用recv函数,如果缓冲区没有数据,这个函数会一直等待,知道数据返回。而在此时,这个线程还是可以处理其他消息的。
非阻塞:当调用后,不能直接得到结果之前,该函数不能阻塞当前线程,而是会立刻返回。
总结:
同步是指A调用了B函数,B函数需要等处理完事情才会给A返回一个结果。A拿到结果继续执行。
异步是指A调用了B函数,A的任务就完成了,去继续执行别的事了,等B处理完了事情,才会通知A。
阻塞是指,A调用了B函数,在B没有返回结果的时候,A线程被CPU挂起,不能执行任何操作(这个线程不会被分配时间片)
非阻塞是指,A调用了B函数,A不用一直等待B返回结果,可以先去干别的事。
Linux下的五种IO模型:
1.阻塞IO
2.非阻塞IO
3.IO复用
4.信号驱动IO
5.异步IO
阻塞IO模型:
从上图可知,因为socket接口是阻塞型的,用户进程会调用recvfrom函数,查看内核里有没有数据报准备好,如果没有,那么只能继续等待,此时用户进程什么也不能做,一直等内核的数据报准备好了,才会将数据报从内核空间复制到用户空间里面,用户进程得到了数据,这个任务才算结束。这就是阻塞型的IO。
非阻塞型IO
用户进程调用了recvfrom函数,向内核要数据报,内核会立刻返回一个结果,如果告诉用户进程没有数据报,那么用户进程还需要继续发送调用请求。。。知道有了数据报,然后复制到用户空间,这样就结束了调用。
非阻塞的IO可能并不会立即满足,需要应用程序调用许多次来等待操作完成。这可能效率不高,因为在很多情况下,当内核执行这个命令时,应用程序必须要进行忙碌等待,直到数据可用为止。
另一个问题,在循环调用非阻塞IO的时候,将大幅度占用CPU,所以一般使用select等来检测”是否可以操作“。
多路复用IO
前面说过非阻塞型IO的缺点,就是占用CPU的资源,使用select函数可以避免非阻塞IO中的轮询等待问题
可以看出用户首先要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回,当数据到达时,socket被激活,select函数返回。这时socket可读了,然后用户线程正式发起read请求,读取数据并继续执行。
这个模型在流程上和同步阻塞模型好像没有区别,甚至还需要监听socket,但使用了select以后最大的优势就是用户可以在一个线程内同时处理多个socket的IO请求,用户可以注册多个socket,然后不断的调用select读取被激活的socket,可以达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须要使用多线程,线程池技术来实现。
{ select(socket); while(1) { sockets = select(); for(socket in sockets) { if(can_read(socket)) { read(socket, buffer); process(buffer); } } } }
但是上面的模型仍然有很大的问题,虽然单个线程可以处理多个IO请求,但每个IO请求也是阻塞的。因此可以让用户线程注册自己感兴趣的socket或者Io请求,然后去做自己的事情,等到数据来到的时候,再进行处理
这里是使用Reactor设计模式来实现。
通过Reactor方式,将用户线程轮询IO操作状态的工作统一交给handle_event事件循环进行处理,用户注册事件处理器之后就可以继续执行其他的工作了,而Reactor线程负责调用内核的select函数来检查socket状态。当socket被激活之后,通知响应的用户线程,执行handle_event进行数据读取。由于select函数是阻塞的,因此多路IO复用模型也被称为异步阻塞IO模型。
后面两种IO模型就先不说了。。。
然后来介绍java中的IO模型怎么实现。
BIO(Blocking IO)
同步阻塞IO模型,数据的读取写入必须阻塞在一个线程内等待完成。
在BIO通信模型的服务端,由一个独立的Acceptor线程负责监听客户端的连接,
如上图所示,如果想要处理多个线程,则必须使用多线程,因为socket.accept()、socket.read()、socket.write()这三个函数都是同步阻塞的。
在使用了多线程之后,服务端接收到客户端的连接请求之后,会为每一个客户端创建一个新的线程进行链路处理。处理完成后,通过输出流返回应答客户端,然后线程销毁。也可以通过线程池来改善性能。利用线程池可以实现N(客户端请求数量):M(处理客户端请求的线程数量)的伪异步I/O模型(N 可以远远大于 M)。
Acceptor监听客户端请求,每有一个新的请求都会通过线程池创建一个新的线程,然后将socket套接字封装成一个task继承runnable,丢到线程里去执行。线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
但问题也很明显,仍然占用了大量的资源。其底层是BIO的事实还是没有改变。
在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
public class ServerMain { public static void main(String[] args) throws IOException { //绑定端口 ServerSocket serverSocket=new ServerSocket(3333); new Thread(()->{ //accept监听 while(true) { try { Socket socket = serverSocket.accept(); //这里发生了阻塞 Thread.sleep(10000); // 按字节流方式读取数据 try { int len; byte[] data = new byte[1024]; InputStream inputStream = socket.getInputStream(); // 按字节流方式读取数据 while ((len = inputStream.read(data)) != -1) { System.out.println(new String(data, 0, len)); } } catch (IOException e) { } } catch (Exception e) { e.printStackTrace(); } } }).start(); } }