深入IO之 IO的工作机制 & IO模型

来源:互联网 发布:域名怎么注册? 编辑:程序博客网 时间:2024/05/14 16:26

基础

一、流

1.读写字节(视频、图片)

 
public abstract class InputStream implements Closeable {
  abstract int read(); //从数据中读入一个字节,并返回该字节
  int read(byte[] b);//读入一个数组
  int read(byte[] b,int off,int len);
  int available();//返回在不阻塞的情况下可获得的字节数
  void close();
  void mark(int readlimit);//在输入流的当前位置打一个标记
  void reset();//返回到最后一个标记
}
/**
关闭一个输出流的同时还会冲刷用于该输出流的缓冲区:所有被临时置于缓冲区中,以便用更大的包的形式传递的字符在关闭输出流时都将被送出
*/
public abstract class OutputStream implements Closeable, Flushable {
  abstract void write(int n);//向某个输出位置写出一个字节的数据
  void write(byte[] b);//写出一个数组
  void write(byte[] b,int off,int len);//
  void close();//冲刷并关闭输出流
  void flush();//冲刷
}

2. 组合流过滤器(装饰器模式)

  • 可以通过嵌套过滤器来添加多重功能,从外到内的流序列来读取,过滤流的创建是通过一个已经存在的流进行创建的

  • FileInputStream & FileOutputStream节点流,提供附着在一个磁盘文件上的输入流和输出流,在构造是只需要提供文件名或者文件的完整路径即可。

  • DataInputStream & DataOutputStream 可以以二进制格式读写所有的基本java类型 的过滤流

  • BufferedInputStream & BufferedOutputStream缓冲过滤流:不带缓冲的操作,每读一个字节就要写入一个字节,由于涉及磁盘的IO操作相比内存的操作要慢很多,所以不带缓冲的流效率很低带缓冲的流,可以一次读很多字节,但不向磁盘中写入,只是先放到内存里。等凑够了缓冲区大小的时候一次性写入磁盘,这种方式可以减少磁盘操作次数,速度就会提高很多

二、文本的输入与输出

  • 尽管 二进制格式的 I/O 高速且高效,但是不宜阅读。所以在存储文本字符串时,可以通过文本格式的 I/O

1.OutputStreamWriter & InputStreamReader

  • OutputStreamWriter:使用选定的字符编码方式,把字节流转换为 Unicode字符 流

  • InputStreamReader:将包含字节的输入流转换为可以产生 Unicode 码元读入器

    • InputStreamReader in =  new InputStreamReader(System.in);

2.对于 Unicode 文本,可以使用抽象类 Reader 和 Writer 的子类。也可以使用字节流,但是效率低

  • PrintWriter:文本输出,其中含有一个Writer变量。当传入的是一个OutputStream时,会先转换为Writer

3. 为什么会有OutputStreamWriter??

  • 通过适配器模式,把字节流转换为字符流,因此可以调用writer的方法: write(String str, int off, int len)、write(char cbuf[], int off, int len)

    图 5. 字符解码相关类结构

4. 字节流和字符流的主要区别是什么?

  • 字节流在操作时不会用到缓冲区(内存),是直接对文件本身进行操作的。而字符流在操作时使用了缓冲区,通过缓冲区再操作文件

  • 在硬盘上的所有文件都是以字节形式存在的,而字符值是在内存中才会形成。那问什么打开文件时能够看见汉字,因为打开文件后已加载进了内存中

1. 所有文件的存储都是字节存储,在磁盘上保留的并不是文件的字符而是先把字符编码成字节,再存储这些字节到磁盘。在读取文件(特别是文本文件)时,也是一个字节一个字节的读取出来。字节流可用于任何类型的对象,包括二进制对象,而字符流 只能处理字符或者字符串。

不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以 I/O 操作的都是字节而不是字符,但是为啥有操作字符的 I/O 接口呢?这是因为我们的程序中通常操作的数据都是以字符形式,为了操作方便当然要提供一个直接写字符的 I/O 接口,如此而已。

2. 字节流不能直接处理字符,而字符流可以

3. 字节流是最基本的,所有的 InputStream 和 OutputStream的子类都是,主要用在处理二进制数据,它是按字节来处理的

4.实际中很多数据是文本,又提出了字符流的概念,它是按虚拟机的encode 来处理,也就是要进行字符集的转化,这两个之间通过InputStreamReader ,OutputStreamWriter来关联,实际上是通过 byte[] 和 String来关联

三、读写二进制数据

1.DataInputStream 和 DataOutputStream 可以以二进制格式读写所有的基本java类型

四、对象流与序列化

1.对象流

  • 对象流可以将任何对象写出到流中,并在之后将其读回,但是这些类必须实现Serializable 接口

  • 当某个对象被多个对象共用时,对象流中如何存储呢?

    • 每个对象都是用一个序列号保存的,这就是这种机制之所以称为对象序列化的原因

    • 当遇到的每一个对象引用都关联一个序列号

    • 当第一次遇到时,保存其对象数据到流中

    • 如果某个对象之前已经被保存过,那么只写出 ”与之前保存过的序列号为x的对象相同“。在读回对象是,整个过程是反过来的

    • 对于流中的对象,在第一次玉到其序列号是,构建他,并使用流中数据来初始化它,然后记录这个顺序号和新对象之间的关联


深入

一、I/O 模型

1. 同步 与 异步

  • 同步:相互牵制,两者之间有一定的约束

  • 异步:两者之间无关,互不牵制

2. 阻塞 与 非阻塞

  • 阻塞:发出一个请求,如果条件不满足,会一直等待直到条件满足

  • 非阻塞:发出一个请求,如果条件不满足,则直接返回一个标志信息,而不会一直等待下去

3.IO操作过程

  • 当用户线程发起一个IO请求操作,内核会去查看要读取的数据是否就绪。阻塞IO:如果没有就绪就会一直等到数据就绪;非阻塞IO :没有就绪,就会返回一个标志信息表示当前要读的数据没有就绪。当数据就绪之后,便将数据拷贝到用户线程,这就是一个完整的IO 请求过程

    1. 查看数据是否就绪

    2. 进行数据拷贝(内核数据拷贝到用户线程)

4. 阻塞 IO  & 非阻塞IO

  • 阻塞IO 与 非阻塞IO的区别在于第一个阶段,如果数据没有就绪,是一直等待还是返回一个标志性信息

5. 异步 IO & 非异步IO

  • 同步IO指当一个线程请求进行IO操作是,在IO操作完成之前,该线程会被阻塞。也就是说如果数据没有就绪,需要通过用户线程或者内核不断去轮询数据是否就绪,当数据就绪时,再将数据从内核拷贝到用户线程

  • 异步IO指如果一个线程请求IO操作,IO操作不会导致请求线程被阻塞。也就是说用户线程只负责发出请求,而IO操作的两个阶段是由内核自动完成,然后发送通知告知用户线程 IO 操作已经完成

  • 区别:同步IO和异步IO的关键区别反映在数据拷贝阶段是由用户线程完成还是内核完成

6. 五种 IO 模型

  • 阻塞IO:直到等待数据准备就绪,内核将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态

  • 非阻塞IO:需要用户线程不断的询问内核数据是否就绪,其不会交出 CPU,而会一直占用 CPU

  • 信号驱动模型:在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。个人觉得类似于中断

  • 多路复用IO模型(NIO Reactor模式):只需要使用一个线程就可以管理多个socket。当使用selector.select() 查询通道是否有到达事件,如果没有事件,则一直阻塞在那里。轮询操作是在内核中进行,所以效率比较高

  • 异步IO模型Proactorr模式:用户线程发起 IO 请求后,立刻去做其他事情。内核会等待数据准备完成,然后将数据拷贝到用户线程,当这两个阶段完成后,内核会给用户线程发送一个信号,表示操作已完成。所以用户线程只需要发起一个请求,而不用知道IO操作是如何进行的

二、磁盘IO的工作机制

数据在磁盘的最小藐视是文件,也就是说上层应用程序只能通过文件来操作磁盘上的数据,文件也是操作系统和磁盘驱动器的一个最小单元。在Java中,File类并不代表一个真实存在的文件的对象,当通过指定一个路径描述符时,它就会返回一个代表这个路径相关联的虚拟对象,这个对象可能是一个真实存在的文件或者是一个包含多个文件的目录。

那么何时真正检查一个文件存不存在?当在真要读取这个文件时。比如,在创建一个FileInputStream 对象时,会创建一个FileDescriptor 对象,这个对象就是真正代表一个存在的文件对象的描述,通过这个对象可以直接控制这个磁盘文件

三、Java Socket工作机制

1.建立通信链路

  • 当客户端要与服务端通信,客户端首先要创建一个Socket实例,操作系统会为这个Socket实例分配一个没有被使用的本地端口号,并创建一个包含本地和远程地址和端口号的套接字数据结构,这个数据结构将一直保存在系统中直到这个连接关闭。在创建Socket完成之前,将进行TCP三次握手协议。握手完成后,Socket对象将创建完成

  • 服务端创建一个ServerSocket实例,操作系统会为这个实例创建一个底层数据结构,这个数据结构中包含指定监听的端口号和包含监听的地址通配符。

    之后当调用 accept() 方法时,将进入阻塞状态,等待客户端的请求。当一个新的请求到来时,将为这个连接创建一个新的套接字数据结构,该套接字数据的信息包含的地址和端口信息正是请求源地址和端口。这个新创建的数据结构将会关联到 ServerSocket 实例的一个未完成的连接数据结构列表中,注意这时服务端与之对应的 Socket 实例并没有完成创建,而要等到与客户端的三次握手完成后,这个服务端的 Socket 实例才会返回,并将这个 Socket 实例对应的数据结构从未完成列表中移到已完成列表中。所以 ServerSocket 所关联的列表中每个数据结构,都代表与一个客户端的建立的 TCP 连接。

2. 数据传输

  • 当连接已经建立成功,服务端和客户端都会拥有一个 Socket 实例,每个 Socket 实例都有一个 InputStream 和 OutputStream,正是通过这两个对象来交换数据。当 Socket 对象创建时,操作系统将会为 InputStream 和 OutputStream 分别分配一定大小的缓冲区,数据的写入和读取都是通过这个缓存区完成的。写入端将数据写到 OutputStream 对应的 SendQ 队列中,当队列填满时,数据将被发送到另一端 InputStream 的 RecvQ 队列中,如果这时 RecvQ 已经满了,那么 OutputStream 的 write 方法将会阻塞直到 RecvQ 队列有足够的空间容纳 SendQ 发送的数据。

四、NIO

1. BIO存在的问题

  • 存在阻塞问题,失去CPU的使用权

  • 当使用线程池时,出现阻塞只会阻塞当前的线程而不会影响其他线程,但是对于需要大量HTTP长连接的情况,并且不是每时每刻都在传输数据,这种情况下不可能同时创建这么多线程来保持连接

2. NIO(New IO )的特性

  • 为所有的原始类型提供Buffer缓存支持

  • 使用 Java.nio.charset.Charset作为字符集编码解码解决方案

  • 增加通道对象,作为新的原始 I/O 抽象

  • 提供了基于 Selector 的异步网络 IO

  • NIO 是基于块(Block)的,它以块为基本单位处理数据。在NIO中国,最重要的两个组件是缓冲Buffer 和通道 Channel。缓冲是一块连续的内存块

3. Channel

  • Channel 表示缓冲数据的源头或者目的地,它用于向缓冲读取或者写入数据,是访问缓冲的接口。它是一个双向通道,即可读,也科协。有点类似于Steam,但 Stream是单向的。在应用程序中,不能直接对Channel进行读写操作,而必须通过Buffer来进行

4.Buffer

  • Buffer中的参数:

    • 位置position:下一个要操作的数据元素的位置

    • 容量capacity:缓冲区的总容量的上限

    • 上限limit:缓冲区的实际上限(size?)

  • 当Buffer从写模式转换为读模式是,需要执行 flip 方法,因为此方法重置了当前position为 0 ,并将limit设置到当前position的位置。这样做的目的是防止在读模式中,读到应用程序根本没有进行操作的区域

  • 创建:

    1. ByteBuffer.allocate(length)

    2. ByteBuffer.wrap(array)

  • 重置与清空

    rewind()clear()flip()position置零置零置零mark清空清空清空limit未改动设置为capacity设置为position作用为读取buffer中有效数据做准备为重新写入Buffer做准备读写切换时调用
  • 复制缓冲区:duplicate()

    • 新生成的缓冲区和原缓冲共享相同的内存数据。所以,对任意一方的数据改动都是相互可见的,但两者有独立维护了各自的position、limit和mark

  • 缓冲区分片:slice()

    • 创建新的缓冲区,是父缓冲区的 [position,limit ),与父缓冲区共享数据

  • DirectBuffer:直接分配在物理内存中,并不占用堆空间,虚度比普通的 ByteBuffer 更快——ByteBuffer.allocateDirect()

参考资料:

http://www.cnblogs.com/dolphin0520/p/3916526.html

http://www.ibm.com/developerworks/cn/java/j-lo-javaio/

0 0
原创粉丝点击