当前位置: 首页 > 后端技术 > Java

IO:阻塞和非阻塞,同步和异步

时间:2023-04-01 18:40:47 Java

阻塞和非阻塞。阻塞和非阻塞时,线程都会被挂起。阻塞:当数据还没有准备好时,如果调用了阻塞方法,线程就会被挂起,这会使CPU时间片用完,此时无法处理请求。它需要等待其他线程唤醒,以便该线程执行后续操作或处理其他请求。非阻塞:就是说当数据没有准备好时,即使我调用阻塞方法,线程也不会被挂起,可以处理后续的请求。同步和异步与串行和并行非常相似。假设在一个场景中:需要4个小任务来完成一个大任务。同步方式:需要4个步骤依次进行。注意这里是有先后顺序的,也就是说要完成这一步,需要先完成前置步骤,也就是说下一步要看上一步的执行结果。异步方式:4个步骤可以同时执行,不需要等待其他步骤的执行结果。阻塞和同步最本质的区别是:即使是同步,线程在等待过程中也不会被挂起,不需要放弃CPU时间片。IO中网络编程的基本模型是:Client/Server模型两个进程需要相互通信,服务器需要提供位置信息让客户端找到自己。服务器提供IP地址和侦听端口。客户端以此信息向服务器发起连接建立请求。通过三次握手成功建立连接后,客户端可以通过套接字向服务器发送和接收消息。BIOBIO通信模型采用了一个典型的:请求-响应通信模型。使用BIO通信模型的服务端通常有一个独立的Acceptor线程负责监听客户端的连接。他不负责处理请求,他只是充当委托工作,当他收到请求时,他会为每个客户端创建一个新的线程来处理链接。处理后通过输出流将response返回给客户端,然后销毁线程,回收资源。这种模式最大的问题是缺乏弹性可扩展性。服务端的线程数和客户端的并发访问数是1:1。由于线程是Java虚拟机非常宝贵的资源,当线程本膨胀时,系统的性能会随着并发的增加而按比例下降。而且,还有OOM的风险。当没有内存空间创建线程时,就无法处理客户端的请求,最终导致进程宕机或卡死,无法对外提供服务。最大的问题是:每当客户端请求访问时,都会创建一个线程来处理请求。为了改进这种一线程一连接的模型,后来又演化出一个或多个线程通过线程池消息队列处理N个客户端的模型。在这里,线程池和消息队列都解决了内存空间和线程的问题,并没有从本质上改变同步阻塞通信的本质。因此,这种优化版的BIO也被称为伪异步。伪异步IO使用线程池和任务队列实现一个:伪异步IO通信将客户端的请求封装成一个Task(该任务实现了java.lang.Runnable接口)传递给消息队列。如果通过线程池维护一堆处理线程,消费队列中的消息。处理完成后,就可以通过客户端了。它的资源是可控的。无论客户要求多少,它都不会改变。这也是它的缺点之一。建立连接的accpet方法和读取数据的read方法被阻塞。这意味着,如果一方处理请求或发送请求速度慢,或者网络传输速度慢,都会影响到另一方。当调用OutputStream的write方法写入输出流时,会一直阻塞,直到所有要发送的字节都写入完毕,否则会发生异常。在TCP/IP中,当消息的接收方处理缓慢时,由于消息滑动窗口的存在,其接收窗口会变小,这就是TCP窗口大小。如果这里使用了同步阻塞IO,写操作被长时间阻塞,直到TCP窗口大小大于0或者出现IO异常。那么通信对方响应时间过长导致的级联故障:线程问题:如果所有可用的线程都被故障服务器阻塞,那么后续的所有IO消息都会在队列中排队。队列问题:如果队列是有界队列,队列满后无法处理请求;如果使用无界队列,会有OOM风险。NIONIO,正式名称是newIO,因为相对于之前发布的java.io包来说是new的,但是老的IO库都是blocking的。NewIO类库的目标是让Java支持非阻塞IO。更多人称之为Non-BlockIObuffer。BufferBuffer是一个对象,通常是ByteBuffer类型。任何时候在NIO中操作数据,都需要经过buffer。在NIO库中,所有的数据操作都是使用缓冲区来处理的。读取数据时,直接读入缓冲区(这里不是直接读到某个地方,而是放入缓冲区)。写入数据时,缓冲区实际上是一个数组。通常是一个字节数组ByteBuffer,同样需要维护读写位置,可以通过指针或者偏移量来实现。除了ByteBuffer,还有其他基本类型的缓冲区:CharBuffer:字符缓冲区ShortBuffer:短整型缓冲区IntBuffer:整型缓冲区LongBuffer:长整型缓冲区DoubleBuffer:双缓冲区通常使用ByteBufferchannelChannel网络数据通过Channel读取和读取的最大区别Channel和Stream的写法就是Channel的数据流是双向的,Stream的数据流是单向的,也就是说:使用Channel,可以同时读写,是全双工的Model。(可以想到HTTP1.1HTTP2.0HTTP3.0`websocket`)MultiplexerSelectorSelector是NIO编程的基础,Selector会不断轮询注册在其上的Channel。如果某个Channel发生了读写事件,则表示该Channel准备就绪,将被Selector轮询。然后根据SelectionKey,就可以得到准备好Channel的集合,用于后续的IO操作。一个选择器可以轮询多个通道。JDK是基于epoll而不是传统的select,所以不受handlefd的限制。就是一个线程负责轮询Selector的千万个clients。AIONIO2.0引入了新的异步通道的概念,通过java.util.concurrent.Future类提供了异步文件通道和异步套接字通道的实现,来表示一个异步操作的结果。在执行异步操作时,会传入一个java.nio.channelsCompletionHandler接口的实现类作为操作完成的回调。NIO2.0的异步socket通道是真正的异步非阻塞IO。同步套接字通道:SocketServerChannel异步套接字通道:AsynchronousServerSocketChannel不需要使用多路复用器(选择器)对其注册的通道进行轮询操作,从而实现异步读写。AIO和NIO最大的区别是:异步SocketChannel是一个被动的执行对象。NIO要求我们将通道注册到selector进行顺序扫描,pollingAIO使用Future类实现回调方法:completed和failed。就是探索两个维度:同步/异步阻塞/非阻塞同步/异步判断标准主要是:通道问题阻塞/非阻塞判断标准主要是:选择器问题阻塞的关键点是:连接建立和数据传输BIO(阻塞)是指只有连接建立(accpet)动作完成后才能进行后续操作。NIO(非阻塞)在处理客户端的连接时,可以将对应的通道注册到Selector中。我有Selecotr帮我扫描就绪状态的channel,所以是non-blockingasynchronousnon-blockingIOasynchronousnon-blockingIO:AIO有人也把JDK1.4引入的NIO称为异步非阻塞IO但严格来说说起来,它只能称为非阻塞IO,并不是真正的异步预选器。选择器底层是通过select/poll实现的。虽然使用epoll代替了select/poll,上层API没有改变,但是NIO优化的性能,依然没有改变IO模型。JDK1.7提供的NIO2.0中新增了一个:asynchronoussocketchannel,这才是真正的异步IO。多路复用器SelectorSelector的核心功能:用于轮询注册在其上的Channel。当找到一个就绪的Channel,它会找出它的SelectionKey,然后进行后续的IO操作。在JDK1.4早期,选择器底层基于select/poll技术进行了优化。用epoll代替伪异步IO只是线程层面的优化,IO模型并没有改变。通过处理任务任务队列+线程池处理请求优化资源的方法解决了BIO线程和请求:1对1的关系