Java NIO1:浅谈I/O模型

1、什么是同步?什么是异步?编程

     同步和异步的概念出来已经好久了,网上有关同步和异步的说法也有不少。如下是我我的的理解:设计模式

  同步就是:若是有多个任务或者事件要发生,这些任务或者事件必须逐个地进行,一个事件或者任务的执行会致使整个流程的暂时等待,这些事件没有办法并发地执行;服务器

  异步就是:若是有多个任务或者事件发生,这些事件能够并发地执行,一个事件或者任务的执行不会致使整个流程的暂时等待。网络

     这就是同步和异步。举个简单的例子,假若有一个任务包括两个子任务A和B,对于同步来讲,当A在执行的过程当中,B只有等待,直至A执行完毕,B才能执行;而对于异步就是A和B能够并发地执行,B没必要等待A执行完毕以后再执行,这样就不会因为A的执行致使整个任务的暂时等待。多线程

若是还不理解,能够先看下面这2段代码:并发

void fun1() {
        
} 
    
void fun2() {
        
} 
    
void function(){
    fun1(); 
    fun2(); 
    ..... 
    ..... 
} 

这段代码就是典型的同步,在方法function中,fun1在执行的过程当中会致使后续的fun2没法执行,fun2必须等待fun1执行完毕才能够执行。异步

接着看下面这段代码:socket

void fun1() { 
      
} 
  
void fun2() { 
      
} 
  
void function(){ 
    new Thread(){ 
        public void run() { 
            fun1(); 
        } 
    }.start(); 
      
    new Thread(){ 
        public void run() { 
            fun2(); 
        } 
    }.start(); 
 
    ..... 
    ..... 
}

这段代码是一种典型的异步,fun1的执行不会影响到fun2的执行,而且fun1和fun2的执行不会致使其后续的执行过程处于暂时的等待。async

   事实上,同步和异步是一个很是广的概念,它们的重点在于多个任务和事件发生时,一个事件的发生或执行是否会致使整个流程的暂时等待。我以为能够将同步和异步与Java中的synchronized关键字联系起来进行类比。当多个线程同时访问一个变量时,每一个线程访问该变量就是一个事件,对于同步来讲,就是这些线程必须逐个地来访问该变量,一个线程在访问该变量的过程当中,其余线程必须等待;而对于异步来讲,就是多个线程没必要逐个地访问该变量,能够同时进行访问。函数

  所以,我的以为同步和异步能够表如今不少方面,可是记住其关键在于多个任务和事件发生时,一个事件的发生或执行是否会致使整个流程的暂时等待。通常来讲,能够经过多线程的方式来实现异步,可是千万记住不要将多线程和异步画上等号,异步只是宏观上的一个模式,采用多线程来实现异步只是一种手段,而且经过多进程的方式也能够实现异步。

2、什么是阻塞?什么是非阻塞?

     在前面介绍了同步和异步的区别,这一节来看一下阻塞和非阻塞的区别。

  阻塞就是:当某个事件或者任务在执行过程当中,它发出一个请求操做,可是因为该请求操做须要的条件不知足,那么就会一直在那等待,直至条件知足;

  非阻塞就是:当某个事件或者任务在执行过程当中,它发出一个请求操做,若是该请求操做须要的条件不知足,会当即返回一个标志信息告知条件不知足,不会一直在那等待。

  这就是阻塞和非阻塞的区别。也就是说阻塞和非阻塞的区别关键在于当发出请求一个操做时,若是条件不知足,是会一直等待仍是返回一个标志信息。

  举个简单的例子:

  假如我要读取一个文件中的内容,若是此时文件中没有内容可读,对于阻塞来讲就是会一直在那等待,直至文件中有内容可读;而对于非阻塞来讲,就会直接返回一个标志信息告知文件中暂时无内容可读。

  在网上有一些朋友将同步和异步分别与阻塞和非阻塞画上等号,事实上,它们是两组彻底不一样的概念。注意,理解这两组概念的区别对于后面IO模型的理解很是重要。

  同步和异步着重点在于多个任务的执行过程当中,一个任务的执行是否会致使整个流程的暂时等待;

  而阻塞和非阻塞着重点在于发出一个请求操做时,若是进行操做的条件不知足是否会返会一个标志信息告知条件不知足。

  理解阻塞和非阻塞能够同线程阻塞类比地理解,当一个线程进行一个请求操做时,若是条件不知足,则会被阻塞,即在那等待条件知足。

3、什么是阻塞IO? 什么是非阻塞IO?

  在了解阻塞IO和非阻塞IO以前,先看下一个具体的IO操做过程是怎么进行的。

  一般来讲,IO操做包括:对硬盘的读写、对socket的读写以及外设的读写。

  当用户线程发起一个IO请求操做(本文以读请求操做为例),内核会去查看要读取的数据是否就绪,对于阻塞IO来讲,若是数据没有就绪,则会一直在那等待,直到数据就绪;对于非阻塞IO来讲,若是数据没有就绪,则会返回一个标志信息告知用户线程当前要读的数据没有就绪。当数据就绪以后,便将数据拷贝到用户线程,这样才完成了一个完整的IO读请求操做,也就是说一个完整的IO读请求操做包括两个阶段:

  1)查看数据是否就绪;

  2)进行数据拷贝(内核将数据拷贝到用户线程)。

  那么阻塞(blocking IO)和非阻塞(non-blocking IO)的区别就在于第一个阶段,若是数据没有就绪,在查看数据是否就绪的过程当中是一直等待,仍是直接返回一个标志信息。

  Java中传统的IO都是阻塞IO,好比经过socket来读数据,调用read()方法以后,若是数据没有就绪,当前线程就会一直阻塞在read方法调用那里,直到有数据才返回;而若是是非阻塞IO的话,当数据没有就绪,read()方法应该返回一个标志信息,告知当前线程数据没有就绪,而不是一直在那里等待。

4、什么是同步IO? 什么是异步IO?

     咱们先来看一下同步IO和异步IO的定义,在《Unix网络编程》一书中对同步IO和异步IO的定义是这样的:

  A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes.
  An asynchronous I/O operation does not cause the requesting process to be blocked.

  从字面的意思能够看出:同步IO即 若是一个线程请求进行IO操做,在IO操做完成以前,该线程会被阻塞;

  而异步IO为 若是一个线程请求进行IO操做,IO操做不会致使请求线程被阻塞。

  事实上,同步IO和异步IO模型是针对用户线程和内核的交互来讲的:

  对于同步IO:当用户发出IO请求操做以后,若是数据没有就绪,须要经过用户线程或者内核不断地去轮询数据是否就绪,当数据就绪时,再将数据从内核拷贝到用户线程;

  而异步IO:只有IO请求操做的发出是由用户线程来进行的,IO操做的两个阶段都是由内核自动完成,而后发送通知告知用户线程IO操做已经完成。也就是说在异步IO中,不会对用户线程产生任何阻塞。

     这是同步IO和异步IO关键区别所在,同步IO和异步IO的关键区别反映在数据拷贝阶段是由用户线程完成仍是内核完成。因此说异步IO必需要有操做系统的底层支持。

     注意同步IO和异步IO与阻塞IO和非阻塞IO是不一样的两组概念。

  阻塞IO和非阻塞IO是反映在当用户请求IO操做时,若是数据没有就绪,是用户线程一直等待数据就绪,仍是会收到一个标志信息这一点上面的。也就是说,阻塞IO和非阻塞IO是反映在IO操做的第一个阶段,在查看数据是否就绪时是如何处理的。

5、五种IO模型

在《Unix网络编程》一书中提到了五种IO模型,分别是:阻塞IO、非阻塞IO、多路复用IO、信号驱动IO以及异步IO。下面就分别来介绍一下这5种IO模型的异同。

一、阻塞IO模型

  最传统的一种IO模型,即在读写数据过程当中会发生阻塞现象。

  当用户线程发出IO请求以后,内核会去查看数据是否就绪,若是没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪以后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。

      典型的阻塞IO模型的例子为:

data = socket.read(); 

      若是数据没有就绪,就会一直阻塞在read方法。

二、非阻塞IO模型

     当用户线程发起一个read操做后,并不须要等待,而是立刻就获得了一个结果。若是结果是一个error时,它就知道数据尚未准备好,因而它能够再次发送read操做。一旦内核中的数据准备好了,而且又再次收到了用户线程的请求,那么它立刻就将数据拷贝到了用户线程,而后返回。

     因此事实上,在非阻塞IO模型中,用户线程须要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU

     典型的非阻塞IO模型通常以下:

while(true){ 
    data = socket.read(); 
    if(data!= error){ 
        处理数据 
        break; 
    } 
} 

     可是对于非阻塞IO就有一个很是严重的问题,在while循环中须要不断地去询问内核数据是否就绪,这样会致使CPU占用率很是高,所以通常状况下不多使用while循环这种方式来读取数据。

三、多路复用IO模型

     多路复用IO模型是目前使用得比较多的模型。Java NIO实际上就是多路复用IO。

  在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操做。由于在多路复用IO模型中,只须要使用一个线程就能够管理多个socket,系统不须要创建新的进程或者线程,也没必要维护这些线程和进程,而且只有在真正有socket读写事件进行时,才会使用IO资源,因此它大大减小了资源占用。

  在Java NIO中,是经过selector.select()去查询每一个通道是否有到达事件,若是没有事件,则一直阻塞在那里,所以这种方式会致使用户线程的阻塞。

  也许有朋友会说,我能够采用 多线程+ 阻塞IO 达到相似的效果,可是因为在多线程 + 阻塞IO 中,每一个socket对应一个线程,这样会形成很大的资源占用,而且尤为是对于长链接来讲,线程的资源一直不会释放,若是后面陆续有不少链接的话,就会形成性能上的瓶颈。

  而多路复用IO模式,经过一个线程就能够管理多个socket,只有当socket真正有读写事件发生才会占用资源来进行实际的读写操做。所以,多路复用IO比较适合链接数比较多的状况。

  另外多路复用IO为什么比非阻塞IO模型的效率高是由于在非阻塞IO中,不断地询问socket状态是经过用户线程去进行的,而在多路复用IO中,轮询每一个socket状态是内核在进行的,这个效率要比用户线程要高的多。

  不过要注意的是,多路复用IO模型是经过轮询的方式来检测是否有事件到达,而且对到达的事件逐一进行响应。所以对于多路复用IO模型来讲,一旦事件响应体很大,那么就会致使后续的事件迟迟得不处处理,而且会影响新的事件轮询。

四、信号驱动IO模型

     在信号驱动IO模型中,当用户线程发起一个IO请求操做,会给对应的socket注册一个信号函数,而后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号以后,便在信号函数中调用IO读写操做来进行实际的IO请求操做。

五、异步IO模型

     异步IO模型才是最理想的IO模型,在异步IO模型中,当用户线程发起read操做以后,马上就能够开始去作其它的事。而另外一方面,从内核的角度,当它收到一个asynchronous read以后,它会马上返回,说明read请求已经成功发起了,所以不会对用户线程产生任何block。而后,内核会等待数据准备完成,而后将数据拷贝到用户线程,当这一切都完成以后,内核会给用户线程发送一个信号,告诉它read操做完成了。也就说用户线程彻底不须要知道实际的整个IO操做是如何进行的,只须要先发起一个请求,当接收内核返回的成功信号时表示IO操做已经完成,能够直接去使用数据了。

  也就说在异步IO模型中,IO操做的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,而后发送一个信号告知用户线程操做已完成。用户线程中不须要再次调用IO函数进行具体的读写。这点是和信号驱动模型有所不一样的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,而后须要用户线程调用IO函数进行实际的读写操做;而在异步IO模型中,收到信号表示IO操做已经完成,不须要再在用户线程中调用iO函数进行实际的读写操做。

  注意,异步IO是须要操做系统的底层支持,在Java 7中,提供了Asynchronous IO。

     前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,由于不管是多路复用IO仍是信号驱动模型,IO操做的第2个阶段都会引发用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。

6、两种高性能IO设计模式

    在传统的网络服务设计模式中,有两种比较经典的模式:一种是 多线程,一种是线程池。对于多线程模式,也就说来了client,服务器就会新建一个线程来处理该client的读写事件,以下图所示:

 

     这种模式虽然处理起来简单方便,可是因为服务器为每一个client的链接都采用一个线程去处理,使得资源占用很是大。所以,当链接数量达到上限时,再有用户请求链接,直接会致使资源瓶颈,严重的可能会直接致使服务器崩溃。

  所以,为了解决这种一个线程对应一个客户端模式带来的问题,提出了采用线程池的方式,也就说建立一个固定大小的线程池,来一个客户端,就从线程池取一个空闲线程来处理,当客户端处理完读写操做以后,就交出对线程的占用。所以这样就避免为每个客户端都要建立线程带来的资源浪费,使得线程能够重用。

     可是线程池也有它的弊端,若是链接大可能是长链接,所以可能会致使在一段时间内,线程池中的线程都被占用,那么当再有用户请求链接时,因为没有可用的空闲线程来处理,就会致使客户端链接失败,从而影响用户体验。所以,线程池比较适合大量的短链接应用。

  所以便出现了下面的两种高性能IO设计模式:Reactor和Proactor

  在Reactor模式中,会先对每一个client注册感兴趣的事件,而后有一个线程专门去轮询每一个client是否有事件发生,当有事件发生时,便顺序处理每一个事件,当全部事件处理完以后,便再转去继续轮询,以下图所示:

      从这里能够看出,上面的五种IO模型中的多路复用IO就是采用Reactor模式。注意,上面的图中展现的 是顺序处理每一个事件,固然为了提升事件处理速度,能够经过多线程或者线程池的方式来处理事件。

  在Proactor模式中,当检测到有事件发生时,会新起一个异步操做,而后交由内核线程去处理,当内核线程完成IO操做以后,发送一个通知告知操做已完成,能够得知,异步IO模型采用的就是Proactor模式。

相关文章
相关标签/搜索