Java I/O

作者: skeeey | 来源:发表于2019-02-02 11:01 被阅读0次
Plauen,Germany by @heapdump

Java 的 I/O 库分为以 streams 为核心的 java.io 和以 buffers 和 channels 为核心的 java.nio。应用程序(JVM)的 I/O 操作都是通过系统调用来完成的,下面先介绍应用程序是如何和系统调用交互的,即 I/O 的操作模式,然后再分别介绍 java.iojava.nio

I/O 模式

Synchronous and Blocking

应用执行系统调用,系统调用阻塞应用,应用等待系统调用结果,等待过程中应用不消耗 CPU

synchronous and blocking

Synchronous and Non-Blocking(低效)

应用执行系统调用,系统调用不阻塞应用(立即返回),但应用需轮询系统调用获得结果

synchronous and non-blocking

Asynchronous and Blocking(多路复用)

应用执行带阻塞通知的非阻塞 I/O 的系统调用(select),应用被阻塞通知阻塞,当有 I/O 操作时,由阻塞通知通知应用。和同步的阻塞 I/O 比较,同步的阻塞 I/O 阻塞每一个调用线程,而异步的阻塞 I/O 只阻塞一个通知线程,其它的线程可以以异步的方式处理 I/O,相关的系统调用: select(2)/poll(2)/epoll(7)(level trigger)

asynchronous and blocking
while(1) {
  // wait for ready to read fds
  int res = select(maxfdsize+1, &readfds, NULL, NULL, &timeout);
  if(res > 0) {
    for (int i = 0; i < MAX_CONNECTION; i++) {
      // tests to see if a file descriptor is part of the set
      if (FD_ISSET(allConnection[i], &readfds) {
        handleEvent(allConnection[i]);
      }
    }
  }
}

Asynchronous and Non-Blocking(事件机制)

应用执行系统调用,系统调用不阻塞应用,当系统调用完成后,系统通知应用。相关的系统调用:aio(7)/epoll(7)(edge trigger)

asynchronous and non-blocking

java.io

java.io 包以 streams 为核心,streams 是有序(ordered)的二进制字节(byte)序列,其具有源(input streams)和目的(output streams),Java 按对流的最小处理单位可以分为字节流(Byte streams)和字符流(Character streams),其中字节流一次最少操作一个字节(8 bit),而字符流一次最少操作两个字节(16 bits, Unicode),因为 Java中的一个字符最少由两个字节组成,所以字节流并不适合处理面向字符的流(文本流)。

字节流(Byte streams)

字节流基于两个基类 InputStream OutputStream 进行扩展,这两个基类中定义了基本的流操作 read()write(int b),其中 write 方法只操作参数的低 8 位。InputStreamOutputStream 都是 blocking I/O,并且当在不同的线程中调用同一个 stream 的方法时,当前 stream 会以当前的 stream 的实列作为锁,所以保证了不同的线程中的操作是顺序的,即只当有一个线程操作完成后,另一个线程才可以继续操作,例如,如果两个线程各自尝试从流中读取 数据,则每个读取操作返回的数据是在流中顺序出现的。类似地,如果两个线程正在写入相同的流,那么在每个写操作中写入的字节将顺序的发送到流。

字符流(Character streams)

字符流基于两个基类 ReaderWriter 进行扩展,这两个基类中定义了基本的字符流操作 read(char[] buf, int offset, int count)write(char[] buf, int offset, int count),同字节流一样, ReaderWriter 也都是 blocking I/O,但是字符流的同步机制和字节流的同步机制有一些不同,字符流使用一个 protected 字段(protected Object lock)作为锁,默认情况下,该字段使用 stream 实列(和字节流一样)本身。但是,ReaderWriter 都提供了一个受保护的构造函数,使用该构造函数可以重新定义 stream 的锁。

标准输入输出流: System.inSystem.outSystem.err 比字符流出现的早, 所以它们都是字节流

字节流与字符流的转换

InputStreamReaderOutputStreamWriter 用于将字节流(InputStream/OutputStream)转化为 按照特定字符编码格式(默认为系统编码)的字符流(Reader/Writer),如:

public Reader readArabic(String file) throws IOException {
    InputStream fileIn = new FileInputStream(file);
    return new InputStreamReader(fileIn, "iso-8859-6");
}

使用字节(InputStream)流读取 String, 将 String 按其字符编码转成 byte 数组,然后使用 ByteArrayInputStream 读取 byte 数组

分类

Filter Streams

  • FilterInputStream and FilterOutputStream
  • FilterReader and FilterWriter

Buffered Streams

  • BufferedInputStream and BufferedOutputStream
  • BufferedReader and BufferedWriter

Piped Streams

  • PipedInputStream and PipedOutputStream
  • PipedReader and PipedWriter

管道(Pipes)可以用于在不同线程之间传递数据,并且使用管道的唯一安全方法是使用两个线程:一个用于读取,一个用于写入。当管道填满时,在管道的一端写入会阻塞线程,同样当管道没有可用的输入(空),在管道的一端读取会阻塞线程。

Print Streams

  • 面向字节的 PrintStream
  • 面向字符 PrintWriter

File Streams

  • FileInputStream and FileOutputStream
  • FileReader and FileWriter
  • RandomAccessFile,没有实现 InputStream/OutputStream,而是实现了 DataOutputDataInput 接口

File,用来描述文件,而 File Streams 与 RandomAccessFile 用来读写文件

Object Streams

Object streams (ObjectInputStream and ObjectOutputStream)主要用于对象的序列化,对象的序列化是实现 RPC/RMI 的基础,Java 中一个可序列化的对象需要实现 Serializable 接口,该接口没有方法声明,但需要提供序列化版本 ID(Serial Version UID),如果需要对对象的序列化特别处理时需要实现以下接口:

  • private void writeObject(ObjectOutputStream out) throws IOException,用于对象序列化
  • private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException,用于对象反序列化
  • private void readObjectNoData() throws ObjectStreamException;

JVM 对对象进行序列化和反序列化时通过反射机制来来调用这些接口

Serial Version UID (unique identifier) 是一个 64-bit long 型的值,默认是对象类的 secure hash 串, 用于标记该对象的定义(类)是否放生变化

对象的序列化过程比较简单,即把一个对象变成字节流即可(writeObject),注意类的 static 变量和用 transient 声明的变量不参与序列化,而反序列则比较复杂,具体的过程如下

  1. JVM 首先加载要反序列化的类,如果加载失败则抛出 ClassNotFoundException
  2. 接着检查 Serial Version UID,如果不一致这抛出 InvalidClassException
  3. 然后 ObjectInputStream 为对象分配内存,并沿着对象的继承关系找到第一个没有实现 Serializable 接口的类,并且调用该类的无参构造函数(这意味着如果父类中没有无参的构造函数,则子类不能实现 Serializable 接口,同时该类也不能被序列化和反序列化 ),然后依次调用子类的构造函数并进对子类进行反序列化操作(readObject),所以 readObject 的行为基本上和构造函数的行为是一样的

实现 Serializable 接口的最佳实践:

  • 考虑类的兼容性和扩展性,对于接口和有继承关系的类应该尽量的避免实现 Serializable 接口,对于 inner class 不应该实现 Serializable 接口
  • 区分一个对象的逻辑数据和物理数据,物理数据是对象的原始数据,而逻辑数据则可以通过对象的其它属性计算获得的,然后决定是否需要特别处理对象的序列化过程,即实现 writeObject/readObject
  • 安全性问题,避免直接序列化敏感数据

Java NIO

The "n" in nio is commonly understood as meaning "new" (the nio package predates the original stream-based io package), but it originally stood for "non-blocking"

NIO 核心组件的包括:

  • Buffers,作为数据容器用来缓存数据
  • Charsets,用来定义字符集,它们对应的解码器(decoders)和编码器(encoders)用来转换字节和字符
  • 各种 Channels,用来抽象 I/O 实体,如 files 和 sockets
  • Selectable Channel 的 SelectorsSelectionKey,用来实现 channels 的多路操作(multiplexed)和 non-blocking I/O 的功能

Buffers

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer
  • ByteOrder
  • MappedByteBuffer

buffer 使用 capacity 表示 buffer 的容量,position 表示当前 buffer 的读写位置(初始为 0,取值范围 0 到 capacity - 1),limit 表示最多可读写的数据量(写模式下等于 capacity),buffer 的基本操作包括:

  • allocate(int capacity) 为 Buffer 分配空间(写模式),为一个 buffer allocate 空间时,分为两种方式:direct 和 non-direct,其中 non-direct 为默认方式,由 JVM 控制内存的使用,而 direct 方式,JVM 直接调用系统的 API 分配内存(堆外内存),JVM 不保证控制内存的使用,如:MappedByteBuffer,其使用 direct buffer,JVM 通过将文件映射到虚拟内存 mmap(2) 来读写文件,JVM 对其的控制具有不确定性,适用于处理大文件
  • flip() 将 buffer 从写模式切换到读模式,即将 position 置 0,然后将 limit 切换到写模式的 position,而
  • clear()compact() 清空缓冲区,其中 clear() 方法清空整个缓冲区,compact() 方法只清除已经读过的数据
  • rewind(),重读 Buffer,将 position 重置为 0,limit 保持不变
  • position()limit(),用来读写 position 与 limit
  • mark()reset()mark() 方法标记 Buffer 中的一个特定 position(0 <= mark <= position <= limit <= capacity),之后通过 reset() 方法恢复到先前标记的 position
  • equals()compareTo() 比较两个 Buffer

Channels

  • FileChannelAsynchronousFileChannel
  • SocketChannel/ServerSocketChannelAsynchronousSocketChannel/AsynchronousServerSocketChannel
  • DatagramChannel
  • Pipe.SinkChannelPipe.SourceChannel

Selectable Channels

继承自 SelectableChannel,可以通过 Selector 监听多个数据通道(channels),channel 以异步的方式从 Buffer 中读写数据,从而实现以 多路复用(multiplexed)的方式操作 I/O (Asynchronous Blocking)或以事件驱动的方式操作 I/O (Asynchronous Non-Blocking)

NIO 的操作模式

一个 selectable channel 要么在阻塞(blocking)模式下,要么在非阻塞(non-blocking)模式。阻塞模式下,channel 上的的每个 I/O 操作都会阻塞。在非阻塞模式下,I/O 操作永远不会阻塞。channel 的阻塞模式可以通过 isBlocking 方法来确定。

Asynchronous Blocking 模式

  1. 通过 Selectoropen() 方法打开一个 selector
  2. 向 selectable channel 中 register() 该 selector
  3. 通过循环调用该 selector.select() 来选择就绪(ready)的 channels
  4. 通过 selector.selectedKeys() 获得就绪的 SelectionKey 的集合
  5. 遍历 SelectionKey 集合,通过自定义的事件处理器(event handler) 来处理 SelectionKey,通常使用独立的线程来运行事件处理器
  6. 最后 close() selector

Asynchronous Non-Blocking 模式

Java7 开始支持 Asynchronous Non-Blocking 模式(AsynchronousFileChannelAsynchronousSocketChannel/AsynchronousServerSocketChannel),实现上,应用可用向 channel 注册事件处理器(实现了 CompletionHandler 接口),当事件发生时,由系统去调用对应的事件处理器

AsynchronousFileChannel 还提供了返回 Future<Integer> 的方法

参考

相关文章

  • 线上学习资料汇总_新

    1. Java I/O 这些年一直记不住的 Java I/O 深入分析 Java I/O 的工作机制 NIO 入门...

  • NIO.2-简介

    More New I/O Java 7 中的 More New I/O APIs,通过在 java.nio.cha...

  • Java & Groovy & Scala & Kotlin -

    Overview 文章介绍的 I/O 主要用于本地文件读写,不涉及网络 I/O。 Java 篇 Java 中的 I...

  • I/O

    Java I/O 总结 理解Java中字符流与字节流的区别 深入学习 Java 序列化 Java I/O 操作及优...

  • 知识总结:I/O模型基础

    I/O基础 1、java1.4之前,java对I/O支持不完善,存在以下问题: 没有数据缓冲区,I/O性能存在问题...

  • Netty 权威指南笔记(一):网络 I/O 模型和 Java

    Netty 权威指南笔记(一):网络 I/O 模型和 Java IO 入门 Java I/O 模型的实现离不开底层...

  • Java I/O

    Java I/O主要包括三个部分1.流式部分2.非流式部分3.其他类 1.字符流 字符流处理文本数据,在java中...

  • java I/O

    一.文件/文件夹File 二.InputStream/OutputStream字节输入输出流 三.关闭流的方式 四...

  • Java I/O

    如果当初陪着你的人走远了,那些日子回忆起来会模糊吗? mark 标记reset 重置exist 存在modifie...

  • Java I/O

    流可以理解为一个有顺序的,有起点和终点的数据序列,输入流表示从一个源读取数据,输出流表示向一个目标写数据。 字节流...

网友评论

      本文标题:Java I/O

      本文链接:https://www.haomeiwen.com/subject/eykssqtx.html