I/O操作分为磁盘I/O操作和网络I/O操作。
InputStream的read操作:

  • JVM会发出read()系统调用,并通过read系统调用向内核发起读请求;
  • 内核向硬件发送读指令,并等待读就绪;
  • 内核把将要读取的数据复制到指向的内核缓存中;
  • 操作系统内核将数据复制到用户空间缓冲区,然后read系统调用返回。

在这个过程中,数据先从外部设备复制到内核空间,再从内核空间复制到用户空间,这就发生了两次内存复制操作。这种操作会导致不必要的数据拷贝和上下文切换,从而降低I/O的性能。

问题:

如果没有数据就绪,这个读取操作将会一直被挂起,用户线程将会处于阻塞状态。

优化I/O操作

1. 使用缓冲区优化读写流操作

NIO与传统 I/O 不同,它是基于块(Block)的,它以块为基本单位处理数据。在NIO中,最为重要的两个组件是缓冲区(Buffer)和通道(Channel)。Buffer是一块连续的内存块,是 NIO 读写数据的中转地。Channel表示缓冲数据的源头或者目的地,它用于读取缓冲或者写入数据,是访问缓冲的接口。
传统I/O和NIO的最大区别就是传统I/O是面向流,NIO是面向Buffer。Buffer可以将文件一次性读入内存再做后续处理,而传统的方式是边读文件边处理数据。

2. 使用DirectBuffer减少内存复制

NIO的Buffer除了做了缓冲块优化之外,还提供了一个可以直接访问物理内存的类DirectBuffer。

3. 多路复用器(Selector)

Selector是Java NIO编程的基础。用于检查一个或多个NIO Channel的状态是否处于可读、可写。
Selector是基于事件驱动实现的,我们可以在Selector中注册accpet、read监听事件,Selector会不断轮询注册在其上的Channel,如果某个Channel上面发生监听事件,这个Channel就处于就绪状态,然后进行I/O操作。
一个线程使用一个Selector,通过轮询的方式,可以监听多个Channel上的事件。我们可以在注册Channel时设置该通道为非阻塞,当Channel上没有I/O操作时,该线程就不会一直等待了,而是会不断轮询所有Channel,从而避免发生阻塞。
**
目前操作系统的I/O多路复用机制都使用了epoll,相比传统的select机制,epoll没有最大连接句柄1024的限制。所以Selector在理论上可以轮询成千上万的客户端。