理解java IO操作中的阻塞非阻塞同步异步

以下均以读数据为例进行说明,写数据原理类似

  • 阻塞和非阻塞
    阻塞和非阻塞用于描述操作过程中线程的状态
    阻塞就是指,IO操作开始时,线程变为阻塞状态,并一直持续到整个IO操作结束(数据读取到用户线程缓冲区)为止;相对的,非阻塞则是指避免操作全程阻塞,参考下图


    理解java IO操作中的阻塞非阻塞同步异步_第1张图片
    image
  • 同步和异步
    同步和异步用于描述api风格
    同步指所有的指令或函数调用完全按顺序执行,上一步操作未完成就不能开始下一步;异步则是未确认上一步操作完成的情况下,依然开始下一步。简单粗暴的理解就是,带回调就是异步(Promise或协程都是基于回调的封装),没有回调就是同步
  • POSIX标准中定义的5种IO模式
    1. 阻塞IO


      理解java IO操作中的阻塞非阻塞同步异步_第2张图片
      image

      全程阻塞,高并发时必须为每个客户端分配单独线程,大量线程阻塞,浪费内存,cpu上下文切换开销也高

    2. 非阻塞IO


      理解java IO操作中的阻塞非阻塞同步异步_第3张图片
      image.png

      与阻塞IO的区别在于,将IO操作分成两步,第一步网卡从远端接收,此步骤完成前,线程轮询网卡状态,直至数据报就绪,然后进行第二步,读取数据到线程(阻塞)。这里的“非阻塞”指的是在耗时较长的第一阶段避免持续阻塞。两次轮询之间,线程会主动阻塞一小段时间,避免无意义的死循环,因此如果只执行一个IO任务,总的阻塞时间并不会减少。虽然对单个任务没有优化,但在高并发时,可以用一个或几个专门的线程来处理IO操作,这些线程在每次轮询时并不是只查看一个数据请求的状态,而是查询一个列表,只要有任意一个请求的数据报可用,则读取并通知业务线程继续其他操作。与之相配合,业务线程需要将任务拆分,每个IO操作前后是不同的子任务,放在队列中等待执行,遇到IO操作则交给前面的IO专用线程处理,这样即使有大量的IO操作,也只需要少数几个线程进行轮询就可以处理,大大降低了线程数,也就节约了内存和CPU上下文切换时间,缺点则是轮询列表也要占用CPU时间,列表较大时这个开销不可忽视

    3. 多路复用IO


      理解java IO操作中的阻塞非阻塞同步异步_第4张图片
      image.png

      在非阻塞IO基础上的改进,将第一阶段的轮询操作放在了内核线程,即操作系统提供的select/poll/epoll,其中epoll效率最高

    4. 信号驱动IO


      理解java IO操作中的阻塞非阻塞同步异步_第5张图片
      image.png

      与非阻塞IO和多路复用IO相比,第一阶段线程可以不再进入阻塞状态,而是向内核注册一个信号处理函数(回调式api),注册后可以立即继续执行其他任务。这里的信号一般就是指SIGIO信号,由于此信号在TCP连接中触发过于频繁(很多情况都会触发),不能很好的区分是否为数据报准备完毕,故一般只在UDP连接中使用。

    5. 异步IO


      理解java IO操作中的阻塞非阻塞同步异步_第6张图片
      image.png

      全程无阻塞,提交IO请求后内核将会把数据复制到用户空间再通知用户线程进行下一步处理,此时用户线程不再需要额外切换到阻塞状态等待数据复制,可以减少一次上下文切换,缺点是需要提前设定好缓冲区大小以供内核复制数据。性能与epoll实现的非阻塞IO同级或略高,目前只有windows上的实现(iocp),linux将于5.0中实现(io_uring)

以上1-4统称为同步IO,1为全程同步,2-4第二阶段复制数据的api是同步的

阻塞状态的线程不占用cpu,之所以要减少阻塞是因为阻塞状态的线程不能用于执行其他任务,只能(连同堆栈一起)暂存等待,当并发任务多时线程数就不可避免的暴增,增加了内存使用率和唤醒线程时的CPU上下文切换开销。而反过来,只要线程不阻塞(无论使用同步还是异步api),就可以在不切换上下文的情况下立即进行下一个并发任务,大大提高了效率

你可能感兴趣的:(理解java IO操作中的阻塞非阻塞同步异步)