同步非阻塞IO---NIO

高并发引起的问题

一个使用传统阻塞I/O的系统,如果还是使用传统的一个请求对应一个线程这种模式,一旦有高并发的大量请求,就会有如下问题:

  1. 线程不够用, 就算使用了线程池复用线程也无济于事;
  2. 阻塞I/O模式下,会有大量的线程被阻塞,一直在等待数据,这个时候的线程被挂起,只能干等,CPU利用率很低,换句话说,系统的吞吐量差;
  3. 如果网络I/O堵塞或者有网络抖动或者网络故障等,线程的阻塞时间可能很长。整个系统也变的不可靠;

概述

  1. 指JDK1.4及以上版本里提供的新的API,为所有原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络。
  2. NIO的作用:进行数据传输
  3. NIO的组件:Buffer,Channel,Selector
    1
    2
    3
    4
    // IO的分类
    BIO - BlockingIO - 同步阻塞式IO
    NIO - NewIO - NonBlockingIO - 同步非阻塞IO - JDK1.4
    AIO - AsynchronousIO - 异步非阻塞式IO - JDK1.8
    NIO

阻塞与同步

点击了解烧水壶例子

同步:
所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。
例如普通B/S模式(同步):提交请求->等待服务器处理->处理完毕返回 这个期间客户端浏览器不能干任何事

异步:
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
例如 ajax请求(异步): 请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕

阻塞:
阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。
有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回,它还会抢占cpu去执行其他逻辑,也会主动检测io是否准备好。

非阻塞
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

再简单点理解就是:

  1. 同步:就是我调用一个功能,该功能没有结束前,我死等结果。
  2. 异步:就是我调用一个功能,不需要知道该功能结果,该功能有结果后通知我(回调通知)
  3. 阻塞:就是调用我(函数),我(函数)没有接收完数据或者没有得到结果之前,我不会返回。
  4. 非阻塞:就是调用我(函数),我(函数)立即返回,通过select通知调用者

同步IO和异步IO的区别就在于:数据拷贝的时候进程是否阻塞
阻塞IO和非阻塞IO的区别就在于:应用程序的调用是否立即返回
综上可知:
同步和异步,阻塞和非阻塞,有些混用,其实它们完全不是一回事,而且它们修饰的对象也不相同。

BIO与NIO对比

IO模型 BIO NIO
方式 从硬盘到内存 从内存到硬盘
通信 面向流 面向缓存
处理 阻塞式IO 非阻塞IO
触发 选择器(轮询机制)
  1. BIO缺点
    a.阻塞:导致效率整体降低。
    b.一对一连接:客户端每过来一个请求,那么在服务端就需要一个线程去处理这个请求。如果客户端产生大量的请求,会导致服务端也产生大量的线程去处理请求,服务器端的线程数量一旦过多,会导致服务器的卡顿甚至崩溃。
    c.如果客户端连接之后不产生任何操作,依然会占用服务端的线程,导致服务器资源的浪费。
  2. 面向流与面向缓冲
    Java NIO和IO之间第一个最大的区别是,IO是面向流的.NIO是面向缓冲区的。Java IO面向流意味着毎次从流中读一个成多个字节,直至读取所有字节,它们没有被缓存在任何地方,此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的教据,需要先将它缓存到一个缓冲区。Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,霱要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数裾。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

Buffer - 缓存区

  1. 作用存储数据
  2. 容器:数组,针对基本类型提供了其中对应的Buffer(boolean类型除外)
  3. 重要位置: capacity >= limit >= position >= mark
    Buffer
    容量(Capacity):缓冲区能够容纳的数据元素的最大数量。这一个容量在缓冲区创建时被设定,并且永远不能改变。
    上界(Limit):限制位。用于限制操作位所能达到的最大位置。缓冲区刚创建时,limit和capacity重合
    位置(Position):下一个要被读或写的元素的索引。用于执行要读写的位置。默认pisition为0。
    标记(Mark):下一个要被读或写的元素的索引。类似于数组的下标,用于执行要读写的位置。默认情况下mark为-1
  4. 重要操作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // flip:翻转缓冲区
    public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
    }

    // clear:清空缓冲区
    public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
    }

    // reset:重置缓冲区
    public final Buffer rewind() {
    postion = 0;
    mark = -1;
    return this;
    }

Channel - 通道

  1. 作用:传输数据。
  2. 分类:
    a.文件 - FileChannel
    b.UDP - DatagramChannel
    c.TCP - SocketChannel、ServerSocketChannel
  3. 通道默认时阻塞的,可以手动设置为非阻塞。
  4. 通道可以进行双向传输。

Selector - 选择器

Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通里已经有可以处理的褕入,或者选择已准备写入的通道。这选怿机制,使得一个单独的线程很容易来管理多个通道。

  1. 作用:选择事件。
  2. 选择器是面向通道进行操作,但是选择器要求通道必须是非阻塞的。
  3. 利用选择器,可以实现一对多的效果。

参考资料