(1)按照流的方向:输入流(inputStream)和输出流(outputStream);
(2)按照实现 功能分:节点流(可以从或向一个特定的地方(节点)读写数据。如 FileReader)和处理流 (是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如 BufferedReader。处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其 他流的多次包装,称为流的链接);
(3)按照处理数据的单位: 字节流和字符流。字节流继 承于 InputStream 和 OutputStream,字符流继承于InputStreamReader 和 OutputStreamWriter 。
序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流 化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决在对对象 流进行读写操作时所引发的问题
序列化的实现,将需要被序列化的类实现Serializable 接口,该接口没有需要实现的方法, implements Serializable 只是为了标注该对象是可被序列化的,然后使用一个输出流(如: File Output Stream)来构造一个 Object Output Stream(对象流)对象,接着,使用 Object Output Stream 对象的 write Object(Object obj)方法就可以将参数为 obj 的对象写出(即保 存其状态),要恢复的话则用输入流。
字节流读取的时候,读到一个字节就返回一个字节;字符流使用了字节流读到一个或多个字节 (中文对应的字节数是两个,在 UTF-8 码表中是 3 个字节)时。先去查指定的编码表,将查 到的字符返回。字节流可以处理所有类型数据,如:图片,MP3,AVI视频文件,而字符流只 能处理字符数据。只要是处理纯文本数据,就要优先考虑使用字符流,除此之外都用字节流。 字节流主要是操作 byte 类型数据,以 byte 数组为准,主要操作类就是 OutputStream、 InputStream字符流处理的单元为 2 个字节的 Unicode 字符,分别操作字符、字符数组或字 符串,而字节流处理单元为 1 个字节,操作字节和字节数组。所以字符流是由 Java 虚拟机将字 节转化为 2 个字节的 Unicode 字符为单位的字符而成的,所以它对多国语言支持性比较好! 如果是音频文件、图片、歌曲,就用字节流好点,如果是关系到中文(文本)的,用字符流好 点。在程序中一个字符等于两个字节,java 提供了 Reader、Writer 两个专门操作字符流的 类。
-
PrintStream类的输出功能非常强大,通常如果需要输出文本内容,都应该将输出流包 装成PrintStream后进行输出。它还提供其他两项功能。与其他输出流不同, PrintStream 永远不会抛出 IOException;而是,异常情况仅设置可通过 checkError 方法测试的内部标志。另外,为了自动刷新,可以创建一个 PrintStream
-
BufferedWriter:将文本写入字符输出流,缓冲各个字符从而提供单个字符,数组和字符 串的高效写入。通过write()方法可以将获取到的字符输出,然后通过newLine()进行换 行操作。BufferedWriter中的字符流必须通过调用flush方法才能将其刷出去。并且 BufferedWriter只能对字符流进行操作。如果要对字节流操作,则使用 BufferedInputStream
-
PrintWriter的println方法自动添加换行,不会抛异常,若关心异常,需要调用 checkError方法看是否有异常发生,PrintWriter构造方法可指定参数,实现自动刷新 缓存(autoflush)
-
节点流 直接与数据源相连,用于输入或者输出
-
处理流:在节点流的基础上对之进行加工,进行一些功能的扩展
-
处理流的构造器必须要 传入节点流的子类
-
流一旦打开就必须关闭,使用close方法
-
放入finally语句块中(finally 语句一定会执行)
-
调用的处理流就关闭处理流
-
多个流互相调用只关闭最外层的流
BIO 就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是 说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调 用时可靠的线性顺序。它的有点就是代码比较简单、直观;缺点就是 IO 的效率和扩展性很 低,容易成为应用性能瓶颈。
是 Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建 多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。
AIO 是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以 人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操 作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续 的操作
同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任 务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可 以保持一致。而异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工 作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是 否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。我们可以用打电话和发短 信来很好的比喻同步与异步操作。
阻塞与非阻塞主要是从 CPU 的消耗上来说的,阻塞就是 CPU 停下来等待一个慢的操作完成 CPU 才接着完成其它的事。非阻塞就是在这个慢的操作在执行时 CPU 去干其它别的事,等这 个慢的操作完成时,CPU 再接着完成后续的操作。虽然表面上看非阻塞的方式可以明显的提高 CPU 的利用率,但是也带了另外一种后果就是系统的线程切换增加。增加的 CPU 使用时间能 不能补偿系统的切换成本需要好好评估。
同/异、阻/非堵塞的组合,有四种类型,如下表:
组 合 方 式 | 性能分析 |
---|---|
同步 非 阻塞 | 提升 I/O 性能的常用手段,就是将 I/O 的阻塞改成非阻塞方式,尤其在网络 I/O 是长连接,同时传输数据也不是很多的情况下,提升性能非常有效。 这种方式通常能提升 I/O 性能,但是会增加CPU 消耗,要考虑增加的 I/O 性能能不能补偿 CPU 的消耗, 也就是系统的瓶颈是在 I/O 还是在 CPU 上。 |
异步 阻塞 | 这种方式在分布式数据库中经常用到,例如在网一个分布式数据库中写一条记录,通常会有一份是同步阻塞的记录,而还有两至三份是备份记录会写到其它机器上,这些 备份记录通常都是采用异步阻塞的方式写 I/O。异步阻塞对网络 I/O 能够提升效率,尤其像上面这种同时写多份相同数据的情况。 |
异 步非 阻塞 | 这种组合方式用起来比较复杂,只有在一些非常复杂的分布式情况下使用,像集群之 间的消息同步机制一般用这种 I/O 组合方式。如 Cassandra 的 Gossip 通信机制就是采用异步非阻塞的方式。它适合同时要传多份相同的数据到集群中不同的机器,同时 数据的传输量虽然不大,但是却非常频繁。这种网络 I/O 用这个方式性能能达到最高。 |
-
通道是对原 I/O 包中的流的模拟。到任何目的地(或来自任何地方)的所有数据都必须通过 一个 Channel 对象(通道)。一个 Buffer 实质上是一个容器对象。发送给一个通道的 所有对象都必须首先放到缓冲区中;同样地,从通道中读取的任何数据都要读到缓冲区 中。
-
Channel是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比 较,通道就像是流。 正如前面提到的,所有数据都通过 Buffer 对象来处理。您永远不会将字节直接写入通道 中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道 中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。
- Buffer 是一个对象, 它包含一些要写入或者刚读出的数据。在 NIO 中加入 Buffer 对 象,体现了新库与原 I/O 的一个重要区别。在面向流的 I/O 中,您将数据直接写入或者 将数据直接读到 Stream 对象中
- 在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中 的。在写入数据时,它是写入到缓冲区中的。任何时候访问 NIO 中的数据,您都是将它 放到缓冲区中。
- 缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。 但是一个缓冲区不 仅仅 是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟 踪系统的读/写进程
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
IO多路复用使用两个系统调用(select/poll/epoll和recvfrom),blocking IO只调用了 recvfrom;select/poll/epoll 核心是可以同时处理多个connection,而不是更快,所以连接 数不高的话,性能不一定比多线程+阻塞IO好,多路复用模型中,每一个socket,设置为nonblocking,阻塞是被select这个函数block,而不是被socket阻塞的。
- select机制
客户端操作服务器时就会产生这三种文件描述符(简称fd):writefds(写)、readfds(读)、和 exceptfds(异常)。select会阻塞住监视3类文件描述符,等有数据、可读、可写、出异常 或超 时、就会返回;返回后通过遍历fdset整个数组来找到就绪的描述符fd,然后进行对应的IO操 作。
优点: 几乎在所有的平台上支持,跨平台支持性好
缺点: 由于是采用轮询方式全盘扫描,会随着文件描述符FD数量增多而性能下降。 每次调用 select(),需要把 fd 集合从用户态拷贝到内核态,并进行遍历(消息传递都是从 内核到用户空间) 默认单个进程打开的FD有限制是1024个,可修改宏定义,但是效率仍然慢。
- poll机制
基本原理与select一致,只是没有最大文件描述符限制,因为采用的是链表存储fd。
- epoll机制
epoll之所以高性能是得益于它的三个函数
- epoll_create()系统启动时,在Linux内核里面申请一个B+树结构文件系统,返回 epoll对象,也是一个fd
- epoll_ctl() 每新建一个连接,都通过该函数操作epoll对象,在这个对象里面修改添加 删除对应的链接fd, 绑定一个callback函数。
- epoll_wait() 轮训所有的callback集合,并完成对应的IO操作
优点: 没fd这个限制,所支持的FD上限是操作系统的最大文件句柄数,1G内存大概支持10万个 句柄 效率提高,使用回调通知而不是轮询的方式,不会随着FD数目的增加效率下降 内核和用户空间mmap同一块内存实现
最传统的一种 IO 模型,即在读写数据过程中会发生阻塞现象。当用户线程发出 IO 请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出 CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除 block 状态。典型的阻塞 IO 模型的例子为:data = socket.read()
;如果数据没有就绪,就会一直阻塞在 read 方法。
当用户线程发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。所以事实上,在非阻塞 IO 模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞 IO不会交出 CPU,而会一直占用 CPU。典型的非阻塞 IO 模型一般如下:
while(true){
data = socket.read();
if(data!= error){
处理数据
break;
}
}
但是对于非阻塞 IO 就有一个非常严重的问题,在 while 循环中需要不断地去询问内核数据是否就绪,这样会导致 CPU 占用率非常高,因此一般情况下很少使用 while 循环这种方式来读取数据。
多路复用 IO 模型是目前使用得比较多的模型。Java NIO 实际上就是多路复用 IO。在多路复用 IO模型中,会有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真正调用实际的 IO 读写操作。因为在多路复用 IO 模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket 读写事件进行时,才会使用 IO 资源,所以它大大减少了资源占用。在 Java NIO 中,是通过 selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。多路复用 IO 模式,通过一个线程就可以管理多个 socket,只有当socket 真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用 IO 比较适合连接数比较多的情况。
另外多路复用 IO 为何比非阻塞 IO 模型的效率高是因为在非阻塞 IO 中,不断地询问 socket 状态时通过用户线程去进行的,而在多路复用 IO 中,轮询每个 socket 状态是内核在进行的,这个效率要比用户线程要高的多
。
不过要注意的是,多路复用 IO 模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用 IO 模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。
在信号驱动 IO 模型中,当用户线程发起一个 IO 请求操作,会给对应的 socket 注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用 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。
NIO 主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统 IO 基于字节流和字符流进行操作,而 NIO 基于 Channel 和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道
NIO 和传统 IO 之间第一个最大的区别是,IO 是面向流的,NIO 是面向缓冲区的。
Java IO 面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。NIO 的缓冲导向方法不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
IO 的各种流是阻塞的。这意味着,当一个线程调用 read() 或write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 NIO 的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞 IO 的空闲时间用于在其它通道上执行 IO 操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
首先说一下 Channel,国内大多翻译成“通道”。Channel 和 IO 中的 Stream(流)是差不多一个等级的。只不过 Stream 是单向的
,譬如:InputStream, OutputStream,而 Channel 是双向的
,既可以用来进行读操作,又可以用来进行写操作。
NIO 中的 Channel 的主要实现有:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
这里看名字就可以猜出个所以然来:分别可以对应文件 IO、UDP 和 TCP(Server 和 Client)。下面演示的案例基本上就是围绕这 4 个类型的 Channel 进行陈述的。
Buffer,故名思意,缓冲区,实际上是一个容器,是一个连续数组。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。
上面的图描述了从一个客户端向服务端发送数据,然后服务端接收数据的过程。客户端发送数据时,必须先将数据存入 Buffer 中,然后将 Buffer 中的内容写入通道。服务端这边接收数据必须通过 Channel 将数据读入到 Buffer 中,然后再从 Buffer 中取出数据来处理。在 NIO 中,Buffer 是一个顶层父类,它是一个抽象类,常用的 Buffer 的子类有:
ByteBuffer、IntBuffer、 CharBuffer、 LongBuffer、 DoubleBuffer、FloatBuffer、ShortBuffer
Selector 类是 NIO 的核心类,Selector 能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。