习惯上,同步往往和阻塞联系在一起,异步和非阻塞联系在一起。实际上这两者是不同的概念,阻塞与非阻塞表述了一个任务在处理问题时的不同状态,是一个点的概念,在任务达到那个点时,根据阻塞或非阻塞进行不同操作。而同步与异步是段的概念,如果是同步则这段程序将在主线程执行,如果是异步则类似多线程的行为。
1.同步与异步
同步(Synchronous)和异步(Asynchronous),在一次方法调用中,如果采用同步的方式,若此方法未完成则整个程序无法进行其他步骤,日常的串行代码类似同步机制,需要一步一步执行。如果再一次方法调用中,进行方法调用后,无需等待方法执行完成即可进行下一步操作,方法在另外一条线上执行不影响本程序,则类似于异步机制。
2. 阻塞和非阻塞
任务在执行的过程中,由于资源不足,而处在一种等待的状态,是阻塞。阻塞状态会一直等待到资源等满足后再执行并返回。非阻塞是指,如果资源满足则进行,如果资源不满足,则返回一个执行不成功的信息,并不等待。
3. IO通信操作中的阻塞与非阻塞
IO通信主要包括:对外部设备的读写、对磁盘内容的读写、socket的读写等操作。这些操作主要分为两个部分:
1)等待IO设备响应任务
2)从IO设备处读取或向IO设备中写入内容
这两个操作都涉及到阻塞或非阻塞概念。
对于阻塞IO而言,等待IO设备响应时会一直等待它直到响应或超时为止。对于非阻塞IO而言,等待IO设备响应时如果发现它不能及时响应则会返回一个未响应信息。
4. IO通信操作中的同步与异步
IO通信主要包括:对外部设备的读写、对磁盘内容的读写、socket的读写等操作。这些操作主要分为两个部分:
1)等待IO设备响应任务
2)从IO设备处读取或向IO设备中写入内容
第一步根据响应情况分为阻塞和非阻塞两种状态,第二步根据情况分为同步与异步两种概念,如果任务程序主动去内核读取或写入数据,主线程因此停滞则为同步IO。
而异步IO是由内核进程引导,内核进行完成了第二部的读写操作,把最后的结果返回给主线程,整个读写过程不是在用户线程上进行,而是在内核上完成,用户进程最后得到通知,这种IO通信叫做异步IO。
5. 五类IO模型
5.1 阻塞IO模型
最传统的IO模型,是java中IO类采用的方法,等待设备时采用阻塞方法,读写信息时进行同步操作。对于以前的串行程序,阻塞IO足够了,时效低下是难免的。
5.2 非阻塞IO模型
顾名思义,当IO设备表示条件不满足时程序会返回一个信息,但为了实现此次IO操作,非阻塞程序需要另外的循环来不断确认IO可操作,这样也会导致CPU效率降低。
5.3 多路复用IO模型
JDK1.4中NIO的IO模型,是一种较为智能的IO模型。在多路复用IO中会有一个线程不断的轮询所有的IO请求操作,如果发现其中的一个处于可用状态则进行IO通信读写操作。在JavaNIO中,是通过selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。
多路复用IO类似于多线程+阻塞IO操作,但是不同的是它只用了一个线程去轮询所有的线程中的IO,效率更高。但也隐含问题,如果一个线程进行中需要完成的任务过多则会导致其他的方法IO不能得到连接。
5.4 信号驱动IO模型
在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。
实际上信号驱动是一种类似于异步+同步的操作,之所以说第一步是异步而不是阻塞或非阻塞,实际上是因为这一步放给内核去完成,程序几乎不用考虑第一步操作。
5.1 异步IO模型
异步IO模型几乎是最好的一种模型,因为在这种模型下两个阶段都不需要用户操作,是一种类似完全异步的状态,用户在发起IO请求后就可以完全不用关心了,可以去执行其他操作。内核会完成接下来的所有事情,一直到2步操作全部完成才会把结果返回给用户。
注意,异步IO是需要操作系统的底层支持,在Java 7中,提供了Asynchronous IO。
前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。
6. 线程池概念
在普通多线程模式下,来了client,服务器就会新建一个线程来处理该client的读写事件。这样随着用户的增多,可能导致连接数量不够用,严重的可能会直接导致服务器崩溃。
因此,为了解决这种一个线程对应一个客户端模式带来的问题,提出了采用线程池的方式,也就说创建一个固定大小的线程池,来一个客户端,就从线程池取一个空闲线程来处理,当客户端处理完读写操作之后,就交出对线程的占用。因此这样就避免为每一个客户端都要创建线程带来的资源浪费,使得线程可以重用。
但是线程池也有它的弊端,如果连接大多是长连接,因此可能会导致在一段时间内,线程池中的线程都被占用,那么当再有用户请求连接时,由于没有可用的空闲线程来处理,就会导致客户端连接失败,从而影响用户体验。因此,线程池比较适合大量的短连接应用。
因此便出现了下面的两种高性能IO设计模式:Reactor和Proactor。
6.1 Reactor
在Reactor模式中,会先对每个client注册感兴趣的事件,然后有一个线程专门去轮询每个client是否有事件发生,当有事件发生时,便顺序处理每个事件,当所有事件处理完之后,便再转去继续轮询。类似于多路复用IO模型。
6.2 Proactor
在Proactor模式中,当检测到有事件发生时,会新起一个异步操作,然后交由内核线程去处理,当内核线程完成IO操作之后,发送一个通知告知操作已完成,可以得知,异步IO模型采用的就是Proactor模式。但是这种模型对内核要求高,在连接数不多且任务执行频繁的情况下,可以采用这种方式。