Java 的 I/O 库分为以 streams 为核心的 java.io
和以 buffers 和 channels 为核心的 java.nio
。应用程序(JVM)的 I/O 操作都是通过系统调用来完成的,下面先介绍应用程序是如何和系统调用交互的,即 I/O 的操作模式,然后再分别介绍 java.io
和 java.nio
I/O 模式
Synchronous and Blocking
应用执行系统调用,系统调用阻塞应用,应用等待系统调用结果,等待过程中应用不消耗 CPU
synchronous and blockingSynchronous and Non-Blocking(低效)
应用执行系统调用,系统调用不阻塞应用(立即返回),但应用需轮询系统调用获得结果
synchronous and non-blockingAsynchronous and Blocking(多路复用)
应用执行带阻塞通知的非阻塞 I/O 的系统调用(select
),应用被阻塞通知阻塞,当有 I/O 操作时,由阻塞通知通知应用。和同步的阻塞 I/O 比较,同步的阻塞 I/O 阻塞每一个调用线程,而异步的阻塞 I/O 只阻塞一个通知线程,其它的线程可以以异步的方式处理 I/O,相关的系统调用: select(2)/poll(2)/epoll(7)
(level trigger)
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)
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 位。InputStream
和 OutputStream
都是 blocking I/O,并且当在不同的线程中调用同一个 stream 的方法时,当前 stream 会以当前的 stream 的实列作为锁,所以保证了不同的线程中的操作是顺序的,即只当有一个线程操作完成后,另一个线程才可以继续操作,例如,如果两个线程各自尝试从流中读取 数据,则每个读取操作返回的数据是在流中顺序出现的。类似地,如果两个线程正在写入相同的流,那么在每个写操作中写入的字节将顺序的发送到流。
字符流(Character streams)
字符流基于两个基类 Reader
和 Writer
进行扩展,这两个基类中定义了基本的字符流操作 read(char[] buf, int offset, int count)
和 write(char[] buf, int offset, int count)
,同字节流一样, Reader
和 Writer
也都是 blocking I/O,但是字符流的同步机制和字节流的同步机制有一些不同,字符流使用一个 protected 字段(protected Object lock
)作为锁,默认情况下,该字段使用 stream 实列(和字节流一样)本身。但是,Reader
和 Writer
都提供了一个受保护的构造函数,使用该构造函数可以重新定义 stream 的锁。
标准输入输出流:
System.in
、System.out
和System.err
比字符流出现的早, 所以它们都是字节流
字节流与字符流的转换
InputStreamReader
与 OutputStreamWriter
用于将字节流(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
andFilterOutputStream
-
FilterReader
andFilterWriter
Buffered Streams
-
BufferedInputStream
andBufferedOutputStream
-
BufferedReader
andBufferedWriter
Piped Streams
-
PipedInputStream
andPipedOutputStream
-
PipedReader
andPipedWriter
管道(Pipes)可以用于在不同线程之间传递数据,并且使用管道的唯一安全方法是使用两个线程:一个用于读取,一个用于写入。当管道填满时,在管道的一端写入会阻塞线程,同样当管道没有可用的输入(空),在管道的一端读取会阻塞线程。
Print Streams
- 面向字节的
PrintStream
- 面向字符
PrintWriter
File Streams
-
FileInputStream
andFileOutputStream
-
FileReader
andFileWriter
-
RandomAccessFile,没有实现
InputStream/OutputStream
,而是实现了DataOutput
和DataInput
接口
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
声明的变量不参与序列化,而反序列则比较复杂,具体的过程如下
- JVM 首先加载要反序列化的类,如果加载失败则抛出
ClassNotFoundException
- 接着检查 Serial Version UID,如果不一致这抛出
InvalidClassException
- 然后
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 的 Selectors 和 SelectionKey,用来实现 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
-
FileChannel
和AsynchronousFileChannel
-
SocketChannel
/ServerSocketChannel
和AsynchronousSocketChannel
/AsynchronousServerSocketChannel
DatagramChannel
-
Pipe.SinkChannel
与Pipe.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 模式
- 通过
Selector
的open()
方法打开一个 selector - 向 selectable channel 中
register()
该 selector - 通过循环调用该
selector.select()
来选择就绪(ready)的 channels - 通过
selector.selectedKeys()
获得就绪的SelectionKey
的集合 - 遍历
SelectionKey
集合,通过自定义的事件处理器(event handler) 来处理SelectionKey
,通常使用独立的线程来运行事件处理器 - 最后
close()
selector
Asynchronous Non-Blocking 模式
Java7 开始支持 Asynchronous Non-Blocking 模式(AsynchronousFileChannel
,AsynchronousSocketChannel/AsynchronousServerSocketChannel
),实现上,应用可用向 channel 注册事件处理器(实现了 CompletionHandler
接口),当事件发生时,由系统去调用对应的事件处理器
AsynchronousFileChannel
还提供了返回Future<Integer>
的方法
网友评论