Java I/O

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> 的方法

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容

  • 前言:在之前的面试中,每每问到关于Java I/O 方面的东西都感觉自己吃了大亏..所以这里抢救一下..来深入的了...
    我没有三颗心脏阅读 2,481评论 0 21
  • 什么是同步?什么是异步?阻塞和非阻塞又有什么区别?本文先从 Unix 的 I/O 模型讲起,介绍了5种常见的 I/...
    java菜阅读 274评论 0 0
  • 1 I/O流的概念,分类2 I/O所有类的结构图及详解3 何为NIO,和传统I/O有何区别4 在开发中正确使用I/...
    艾剪疏阅读 483评论 0 2
  • 创建一个好的输入/输出(I/O)系统是一项艰难的任务。挑战似乎来自于要涵盖所有的可能性。不仅存在各种I/O源端和想...
    王侦阅读 1,134评论 0 2
  • Stream概述 Stream是一个数据流,可以从它读取数据或写入数据。它是连接数据源或数据目的地,例如文件,网络...
    狮_子歌歌阅读 694评论 1 2