I/O 是机器获取和交换信息的主要渠道,而流是完成 I/O 操作的主要方式。
在计算机中,流是一种信息的转换,它是有序的,输入和输出是针对于应用程序而言的。比如当前程序中需要读取文件中的内容就是 输入流(InputStream),而如果需要将应用程序本身的数据发送到其他应用,就是 输出流(OutputStream),合称为输入/输出流(I/O Streams)。
根据流的内容的不同, I/O流又可以被分为 字节流 和 字符流,如下图所示:
那么字节流和字符流有什么区别呢?参考以下表格:
指标 | 字节流 | 字符流 |
---|---|---|
基类 | OutputStream/InputStream | Writer/Reader |
操作单元 | 字节 | 字符(多个字节) |
适用数据 | 所有 | 文本类数据 |
缓冲区 | 可有可无 | 有 |
信息的最小存储单元都是字节, 而 I/O 流操作要分为字节流和字符流操作的原因在于,字符到字节必须经过转码,这个过程非常耗时,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。
我们知道,I/O操作分为 磁盘I/O操作 和 网络I/O操作。
前者是从磁盘中读取数据源输入到内存中,之后将读取的信息持久化输出在物理磁盘上;后者是从网络中读取信息输入到内存,最终将信息输出到网络中。
不管是磁盘I/O还是网络I/O,都是以字节为单位,在高并发、大数据场景中,很容易导致阻塞,因此性能是非常差的。还有,输出数据从用户空间复制到内核空间,再复制到输出设备,这样的操作会增加系统的性能开销,接下来我们来具体分析一下:
1.多次内存复制
其中,User Space 是指操作系统中的用户态,Kernel Space 是指操作系统中的 内核态,定义如下:
用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。
内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。
流程说明:
步骤1:用户态发出 read() 系统调用,向内核发起读请求;
步骤2:内核向硬件发送读指令,并等待读就绪;
步骤3:通过DMA,内核把将要读取的数据复制到指向的内核缓存中;
步骤4:操作系统内核将数据复制到用户空间缓冲区,然后read系统调用返回。
在这个过程中,数据先从外部设备复制到内核空间,再从内核空间复制到用户空间,这就发生了两次内存复制操作。这种操作会导致不必要的数据拷贝和上下文切换,从而降低I/O的性能。
2.阻塞
在传统 I/O 中,InputStream 的 read() 是一个 while 循环操作,它会一直等待数据读取,直到数据就绪才会返回。这就意味着如果没有数据就绪,这个读取操作将会一直被挂起,用户线程将会处于阻塞状态。
在少量连接请求的情况下,使用这种方式没有问题,响应速度也很高。但在发生大量连接请求时,就需要创建大量监听线程,这时如果线程没有数据就绪就会被挂起,然后进入阻塞状态。一旦发生线程阻塞,这些线程将会不断地抢夺CPU资源,从而导致大量的CPU上下文切换,增加系统的性能开销。
Java NIO 提供了与标准 IO 不同的 IO 工作方式:
Channels and Buffers(通道和缓冲区):标准的 IO 基于字节流和字符流进行操作,而 NIO 是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
非阻塞模式:Java NIO 是非阻塞的,每一次数据读写调用都会立即返回,并将目前可读(或可写)的内容写入缓冲区或者从缓冲区中输出,即使当前没有可用数据,调用仍然会立即返回并且不对缓冲区做任何操作。
Selectors(选择器):Java NIO 引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。
下面来具体介绍一下 NIO 中的这些概念:
1. Channel(通道)
Channel 用于读取缓冲或者写入数据,是访问缓冲的接口。它抽象了一个重要特征就是可以通过配置阻塞行为,来实现非阻塞式的通道。
Channel 是一个双向通道,与传统 IO 操作只允许单向的读写不同的是,NIO 的 Channel 允许在一个通道上进行读和写的操作,将数据在管道两端的字节缓冲之间进行高效率的传输。它就像是一个网关,通过它可以用最小的成本来访问操作系统本地的 I/O 服务,而缓冲则是在两端内部的端点,Channel 使用它来发送和接收数据。
2. Buffers(缓冲区)
Buffer 是一块连续的内存块,是 NIO 读写数据的中转地。
Buffer可以将文件一次性读入内存再做后续处理,而传统的方式是边读文件边处理数据。虽然传统I/O后面也使用了缓冲块,例如 BufferedInputStream,但仍然不能和 NIO 相媲美。使用 NIO替代传统 I/O 操作,可以提升系统的整体性能,效果立竿见影。
3.非阻塞模式
所有的Java IO 流都是阻塞的,这意味着,当一条线程执行 read() 或者 write() 方法时,这条线程会一直阻塞知道读取到了一些数据或者要写出去的数据已经全部写出,在这期间这条线程不能做任何其他的事情。
java NIO 的非阻塞模式允许一条线程从 Channel 中读取数据,通过返回值来判断 Buffers 中是否有数据,如果没有数据,NIO 不会阻塞,就去做其他事情了,过一段时间再回来判断一下有没有数据。
NIO 的写也是一样的,一个线程将 Buffers 中的数据写入 Channel,它不会等待数据全部写完才会返回,而是调用完 write() 方法就继续向下执行。
4.Selectors(选择器)
Java NIO 的选择器允许一个单独的线程来监视多个输入通道,可以注册多个通道使用一个选择器。
要使用Selector,需要向 Selector 注册 Channel,然后调用它的 select() 方法。
这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,比如新连接进来,数据接收等,如下图所示: