IO 流简介?
IO 即 Input/Output,输入/输出。将数据读取到内存中即输入,将内存中的数据写入到数据库、文件、远程主机等即输出。根据数据的处理方式又分为字节流和字符流。
Java IO 流的 40 多个类都是由下面 4 个抽象基类中派生出来的。 InputStream/Reader: 所有输入流的基类,前者是字节输入流,后者是字符输入流。 OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
字节流:
常用的字节输入流简介?
InputStream 字节输入流,用于从源头读取数据到内存中,它是所有字节输入流的父类,它的常用方法:
read():返回输入流中的下一个字节的数据。返回值介于 0 ~ 255 之间,如果没有读到任何字节,返回 -1 表示文件结束。read(byte b[]):从输入流中读取一些字节到数组 b 中,如果 b 长度为 0 则不读取,如果没有可读字节,返回 -1。最多可以读取 b.length 长度的字节数。这个方法等价于read(b, 0, b.length)read(byte b[], int off, int len):在read(byte b[])基础上增加了 off 偏移量参数和 len 要读取的最大字节数。skip(long n):忽略输入流中的 n 个字节,返回实际忽略的字节数。available():返回输入流中可以读取的字节数。close():关闭输入流释放相关的系统资源。
从 Java 9 新增加的方法:
readAllBytes():读取输入流中所有的字节,返回字节数组。readNBytes(byte[] b, int off, int len):阻塞读取直到读取 len ge字节。transferTo(OutputStream out):将所有的字节从一个输入流传递给一个输出流。
FileInputSteam 文件输入流,可以从指定的文件路径读取字节数据。 FileInputSteam 一般会配合 BufferedInputStream 来使用示例:
public static void main(String[] args) throws Exception {
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("test.txt"));
String data = new String(bufferedInputStream.readAllBytes());
System.out.println(data);
}
DataInputStream 用于读取指定类型数据,它不能单独使用,必须结合使用,使用示例:
public static void main(String[] args) throws Exception {
DataInputStream dataInputStream = new DataInputStream(new FileInputStream("test.txt"));
boolean b = dataInputStream.readBoolean();
double v = dataInputStream.readDouble();
}
ObjectInputStream 用于从输入流中读取 Java 对象,ObjectOutputStream 用于将对象写入到输出流。
public static void main(String[] args) throws Exception {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("test.txt"));
User user = (User) objectInputStream.readObject();
}
常用的字节输出流?
OutputStream 用于将数据字节写入到目的地,通常是文件,OutputStream 抽象类是所有字节输出流的父类,它的常用方法:
write(int b):将特定字节写入输出流。write(byte b[]):将数组 b 写入到输出流,等价于Write(b, 0, b.length)。write(byte[] b, int off, int len):在write(byte b[])基础上增加了 off 偏移量和 len 要读取的最大字节数。flush():刷新输出流,并强制写出所有缓存的输出字节数。close():关闭输出流释放相关的系统资源。
FileOutputStream 是最常用的字节输出流,可以直接指定文件路径,可以直接输出单字节数据,也可以输出指定的字节数组。
public static void main(String[] args) throws Exception {
try(FileOutputStream fos = new FileOutputStream("file_path")){
String data = "data";
byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
fos.write(bytes);
}
}
FileOutputStream 也可以配合 BufferedOutputSteam 使用:
public static void main(String[] args) throws Exception {
try(FileOutputStream fos = new FileOutputStream("file_path");
BufferedOutputStream bos = new BufferedOutputStream(fos)){
String data = "data";
byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
bos.write(bytes);
}
}
DataOutputStream 用于写入指定类型数据,不能单独使用,比如结合其他流使用,比如 FileOutputStream:
public static void main(String[] args) throws Exception {
try(FileOutputStream fos = new FileOutputStream("file_path");
DataOutputStream dos = new DataOutputStream(fos)){
dos.writeBoolean(true);
dos.writeInt(123);
}
}
ObjectOutputStream 用于将对象写入到输出流:
public static void main(String[] args) throws Exception {
try(FileOutputStream fos = new FileOutputStream("file_path");
ObjectOutputStream oos = new ObjectOutputStream(fos)){
Person person = new Person();
oos.writeObject(person);
}
}
字符流:
不管是文件读写还是网络请求发送接受,信息的最小存储单元室字节,那为什么还要分为字节流和字符流呢? 主要有两个方面的原因:
- 处理效率:对于英文字节和字符是一一对应的,但是对于其他语言比如中文,一个字符往往对应多个字节,直接处理字节效率更高,且不容易出错。
- 编码问题:字节流可以方便操作字节,但是不同的编码方式对字节序列的解释是不一样的,从而容易导致乱码。字节流提供了字节级别的操作,并且能识别编码方式,从而方便字符操作,且不容易产生乱码。
字符流默认采用的是 Unicode 编码,我们也可以通过构造方法自定义编码。
常用的字符编码锁占字节数?
- utf8:英文占 1 字节, 中文占 3 字节;
- unicode:任何字符都占 2 哥字节;
- gbk:英文占 1 字节,中文占 2 字节;
字符输入流 Reader?
Reader 用于从源头(通常是文件)读取字符信息到内存中,Reader 抽象类是所有字符输入流的父类。Reader 用于读文本,InputStream 用于读原始字节。 常用方法:
read():从输入流读取一个字符;read(char[] cbuf):从输入流中读取一些字符,并将它们存储到字符数组 cbuf 中,等价于read(cbuf, 0, cbuf.length);read(char[] cbuf, int off, int len):在read(char[] cbuf)基础上增加了 off 偏移量, len 要读取的最大字符数。skip(long n):忽略输入流中 n 个字符,返回实际忽略的字符数;close():关闭输入流并释放相关的系统资源;
InputStreamReader 是字节流转换为字符流的桥梁,其子类 FileReader 是基于该基础上的封装,可以直接操作字符文件。
public static void main(String[] args) throws Exception {
try(FileInputStream fis = new FileInputStream("file_path");
InputStreamReader isr = new InputStreamReader(fis)){
String encoding = isr.getEncoding();
int read = isr.read();
}
}
public static void main(String[] args) throws Exception {
try (FileReader fr = new FileReader("file_path")) {
long skip = fr.skip(5);
int content;
while ((content = fr.read()) != -1) {
System.out.print((char) content);
}
}
// 假设文本内容是:你好hi,world!
// 输出内容是:world!
}
字符输入流 Writer?
Writer 用于将数据字符信息写入到目的地,比如文件中,Writer 抽象类是所有字符输出流的父类,它的常用方法:
write(int c):写入单个字符;write(char[] cbuf):写入字符数组 cbuf;write(char[] cbuf, int off, int len):在write(char[] cbuf)的基础上增加了 off 偏移量,和 len 最大读取的字符数;write(String str):写入字符串;write(String str, int off, int len):在write(String str)的基础上增加了 off 偏移量和 len 最大读取字符数;append(CharSequence csq):将指定的字符序列附加到指定的 Writer 对象,并返回该对象;append(char c):将指定的字符序列附加到指定的 Writer 对象并返回该对象;flush():刷新此输出流并强制写出所有缓冲的输出字符;close():关闭输出流并释放相关的系统资源;
OutputStreamWriter 是将字符流转换为字节流的桥梁,其子类 FileWriter 是基于该基础上封装的,可以直接将字符写入到文件中。
public static void main(String[] args) throws Exception {
try (FileWriter fw = new FileWriter("file_path")) {
fw.write("hello, world");
}
}
字节缓冲流?
由于 IO 操作很消耗性能,缓冲流将数据加载至缓冲区,一次性读取/写入多个字节,从而避免频繁的 IO 操作,提高流的传输效率。
字节缓冲流采用了装饰器模式来增强 InputStream 和 OutputStream 子类对象的功能。比如:BufferedInputStream bis = new BufferedInputStream(new FileInputStream(test.txt));
字节流和字节缓冲流的性能差别主要体现在我们使用 write(int b) 和 read() 这两个一次只读取一个字节的方法的时候,由于有字节缓冲区,因此字节缓冲流会先将读取的字节存放在缓存区,大幅减少 IO 次数,提高读取效率。如果是调用 read(byte b[]) 和 write(byte b[], int off, int len) 这两个方法读取写入的话,只要字节数组大小合适,两者的性能差距不大。
BufferedInputStream 字节缓冲输入流,它从源头读取数据的时候不会一个字节一个字节的读,而是会先将读取的字节放在缓存区,并从内部缓冲区中单独读取字节,这样大幅减少 IO 次数,提高读取效率。
BufferedOutputStream 字节缓冲输出流,将数据写入到目的地时,不会一个字节一个字节写入,而是会先将要写入的字节放在缓存区,并从缓冲区单独写入字节,这样大幅减少 IO 次数,提高写出效率。
字符缓冲流?
BufferedReader 字符缓冲输入流和 BufferedWriter 字符缓冲输出流,类似与字节缓冲输入/输出流,它们内部维护了一个字符数字作为缓冲区。
打印流
System.out.print("Hello!"); 实际上是用于获取一个 PrintStream 对象,Print 方法实际上调用的是 PrintStream 的 write 方法。PrintStream 属于字节打印流,与之对应的是 PrintWriter 字符打印流。PrintStream 是 OutputStream 的子类, PrintWriter 是 Writer 的子类。
随机访问流
RandomAccessFile 支持随意跳转到文件的任意位置进行读写。 RandomAccessFile 的构造函数如下:
public RandomAccessFile(File file, String mode)
throws FileNotFoundException {
this(file, mode, false);
}
其中读写模式有四种:
r:只读模式;rw:读写模式;rws:相对于读写,同步更新对文件内容或者元数据修改到外部设备;rwd:相对于读写,同步更新对文件内容的修改到外部存储设备;
public static void main(String[] args) throws Exception {
try (RandomAccessFile raf = new RandomAccessFile("file_path", "rw")) {
// 获取当前指针偏移量,结果为 0
long filePointer1 = raf.getFilePointer();
// 读取一个字节
int read = raf.read();
// 再次获取指针偏移量,结果为 1
long filePointer2 = raf.getFilePointer();
// 将指针向后移动 1000,然后再写入,换行符也是字节,如果移动到最后一行后,还不够 1000,就会在当前行一直移动,移动的空隙是 null。
raf.seek(1000);
// 将指针移动到文件头,并写入数据,此时是对数据进行覆盖写。
raf.seek(0);
raf.write("嘻嘻".getBytes(StandardCharsets.UTF_8));
}
}
RandomAccessFile 比较常见的一个应用就是实现大文件的断点续传,所谓断点续传就是上传中途暂停或者失败之后,不需要重新上传,只需要上传那些未成功的文件分片即可。分片上传时断点续传的基础。
Java IO 中用到的设计模式?
-
装饰器模式:在不改变原有对象的情况下,拓展它的功能。 比如我们可以用 BufferedInputStream 来增强 FileInputStream。
-
适配器模式:用于协调(适配)胡不兼容的类(接口)。 适配器模式中,被适配的对象或者类称为适配者,作用于适配者的对象或者类称为适配器。适配器分为对象适配器和类适配器。类适配器使用继承关系来实现,对象适配器是用组合关系来实现。
在 IO 流中,字符流和字节流的接口不同,它们之间可以协调工作就是基于适配器模式来做的,准确的说是对象适配器。通过适配器,我们可以将字节流对象适配成一个字符流对象,这样我们可以直接通过字节流对象来读取或者写入字符流数据。
InputStreamReader 和 OutputStreamWriter 就是两个适配器,InputStreamReader 使用 StreamDecoder 对字节进行解码,实现字节流到字符流的转换。 OutputStreamWriter 使用 StreamEncoder 对字符流进行编码,实现字符流到字节流的转换。 InputStream 和 OutpuStream 的子类是被适配者。InputStreamReader 和 OutputStreamWriter 是适配器。
适配器模式和装饰器模式有什么区别?
装饰器模式更侧重于动态增强原始类的功能,装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口,并且装饰器模式支持对原始类嵌套使用多个装饰器。
适配器模式更侧重于让接口不兼容而不能交互的类可以一起工作,当我们调用适配器对应的方法时,适配器内部会调用适配者类或者和适配者类相关的方法。另外适配器和适配者两者不需要继承相同的抽象类或者实现相同的接口。
工厂模式工厂模式用于创建对象,NIO 中大量使用了工厂模式,比如 Files 类的 newInputStream 方法创建 InputStream, Paths 类的 get 方法创建 Path 对象等。
观察者模式 NIO 中文件目录监听服务使用了观察者模式,NIO 中文件目录监听服务基于 WatchService 接口和 Watchable 接口,WatchService 属于观察者, Watchable 属于被观察者。Watchable 接口定义了一个将对象注册到 WatchService 并绑定监听事件的方法 register。WatchService 用于监听文件目录的变化,同一个 WatchService 对象可以监听多个文件目录。常用的监听事件有:文件创建、文件删除、文件修改。
Java 的 IO 模型?
IO 即输入/输出(Input/Output)。 从计算机结构的角度看,I/O 描述了计算机系统与外部设备之间的通信过程。 从应用程序的角度看,由于进程的地址空间划分为用户空间和内核空间,因此用户进程要进行 IO 操作时,需要发起系统调用来访问内核空间,应用程序发起 IO 调用后会经历两个步骤:1. 内核等待 IO 设备准备好数据;2.内核将数据从内核空间拷贝到用户空间。
有哪些常见的 IO 模型?
unix 系统下,一共有 5 种 IO 模型:同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O、异步 I/O。
Java 种常见的 IO 模型?
-
BIO(Blocking I/O) BIO 属于同步阻塞 IO 模型,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。在客户端连接数量不高的情况下,BIO 是没有问题的,但是对于十万甚至百万级别的连接时,BIO 模型是无能为力的。
-
NIO (Non-Blocking/New I/O) NIO 中的 N 可以理解为 Non-blocking,而不单纯是 New。它是支持面向缓冲的,基于通道的 I/O 操作方法,对于高负载、高并发的应用,应该使用 NIO。
Java 中的 NIO 可以看做是 I/O 多路复用模型。 I/O 多路复用模型中,线程首先发起 select/poll/epoll 系统调用来询问内核数据是否已经就绪,等内核把数据准备好了,用户线程再发起 read 调用,read 调用还是阻塞的将数据从内核空间复制到用户空间。
select 调用:内核提供的系统调用,它支持一次查询多个系统调用的状态,几乎所有的操作系统都支持。 epoll调用:属于 select 调用的增强版本,优化了 IO 的执行效率。
Java 的 NIO 中,有一个非常重要的选择器(selector)概念,也可以被称作多路复用器。通过它,只需要一个线程就可以管理多个客户端链接,当客户端数据到了之后,才会为其服务。
也有人认为 NIO 属于同步非阻塞 IO 模型,同步非阻塞 IO 模型是应用程序会一直(循环)发起 read 调用,在等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核拜数据拷贝到用户空间。相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大的改进,通过轮询操作,避免了一直阻塞。但是这种 IO 模型存在应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程十分消耗 cpu 资源。同时由于 Java 的 选择器(Selector) 概念,以及 Java 的具体实现,更符合 I/O 多路复用模型。
- AIO(Asynchronous I/O) AIO 也就是 NIO2,它是 NIO 的改进版,是异步 IO 模型。 异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会阻塞,当后天处理完成后,操作系统会通知相应的线程进行后续操作。
目前 AIO的应用还不是很广泛, Netty 之前尝试使用过 AIO,不过由于使用了 AIO 后性能没有太多提升,又放弃了 AIO。
NIO 简介?
在传统的 Java I/O 模型中,I/O 是阻塞进行的。因此对于多并发或者大量级的连接时,I/O 会可能成为性能瓶颈。因此 Java 引入了新的 I/O 模型 NIO(New IO 或者叫 Non-blocking IO),它提供了非阻塞、面向缓冲、基于通道的 I/O,可以使用少量的线程来处理多个链接,大大提高 I/O 效率和并发。
NIO 的核心组件?
Buffer(缓冲区): NIO 读写数据都是通过缓冲区来进行操作的,读是将 Channel 中的数据填充到 Buffer 中,写是将 Buffer 中的数据写入到 Channel 中。 Channel(通道): Channel 是一个双向的、可读可写的数据传输通道, NIO 是通过 Channel 来实现数据输入输出的,通道是一个抽象概念,它可以代表文件、套接字或者其他数据源之间的连接。 Selector(选择器): Selector 允许一个线程处理多个 Channel,基于事件驱动的 I/O 多路复用模型。所有的 Channel 都可以注册到 Selector 上,由 Selector 来分配线程来处理事件。
NIO 的 Buffer 组件详细介绍?
在传统的 BIO 中,数据的读写是面向流的,分为字节流和字符流。在 NIO 中,所有数据都是用缓冲区处理的,这有点类似 BIO 中的缓冲流。 NIO 在读数据时,它是直接读到缓冲区中,写数据时写到缓冲区中,因此 NIO 的读写数据都是通过缓冲区进行操作的。
Buffer 类中的四个成员变量解释:
- **mark(标记):**mark 是一个可选属性, Buffer 允许将位置直接定位到该标记处。
- position(位置): Buffer 中下一个可以被读写数据的位置(索引)。当 Buffer 由写切换到读时
flip(),或者当读模式通过clear()切换回写模式时, position 都会归零。 - limit(界限): Buffer 中可以读/写数据的边界。写模式下代表最多能写入的数据,一般等于 capacity,但是可以通过
limit(int newLimit)方法进行设置;读模式下 limit 等于 Buffer 中实际写入的数据大小。 - capacity(容量): Buffer 中可以存储的最大数据量,Buffer 创建时设置且不可改变。
上述四个变量满足如下关系:0 <= mark <= position <= limit <= capacity。
Buffer 核心方法详解:
allocate(int capacity): Buffer 对象不能直接通过 new 创建,只能通过静态方法实例化,allocate 是分配堆内存。allocateDirect(int capacity): allocateDirect 是分配直接内存。flip(): 从写模式切换到读模式,它首先会将当前的 position 值设置给 limit,同时将 position 置为 0。clear(): 从读模式切换到写模式,清空缓冲区,并将 position 值设置为 0, 将 limit 的值设置为 capacity。compact(): 从读模式切换到写模式,将未读数据(position 到 limit 部分的数据)复制到 Buffer 的开头,并将 position 设置为最后一个未读数据之后,同时将 limit 重置为 capacity,为后续写留出空间。get: 读取缓冲区中的数据,读取指针从 position 向 limit 移动。put: 向缓冲区写入数据,写指针从 position 向 limit 移动。
mark 成员变量的详解:
当调用 mark()方法时,会将当前 position 进行标记。后续调用 reset()时,可以将 position 重置为 mark 标记的位置。如果没有调用 mark 就调用 reset 会抛出异常InvalidMarkException。
NIO 的 Channel 组件详细介绍?
Channel 是一个通道,它建立了与数据源(文件、网络套接字)直接的连接。并且它是全双工的,可以用于数据的读、写,或者同时读写。而 BIO 中的流是单向的,分为各种输入流(InputStream)和输出流(OutputStream)。
Channel 最常用的几个类型的通道:
FileChannel: 文件访问通道。SocketChannel、ServerSocketChannel: TCP 通信通道。DatagramChannel: UDP 通信通道。
Channel 最核心的两个方法:
read: 读取数据并写入到 Buffer 中。write: 将 Buffer 中的数据写入到 Channel 中。
public static void main(String[] args) throws IOException {
RandomAccessFile reader = new RandomAccessFile("filePath","r");
FileChannel channel = reader.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
}
NIO 的 Selector 组件详细介绍?
Selector 它允许一个线程处理多个 Channel , Selector 是基于事件驱动的 I/O 多路复用模型。它的大致运作原理:
// 1.打开 Selector
Selector selector = Selector.open();
// 2.将要监听的 Channel 和事件注册到 Selector
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
while(true) {
// 3.监听就绪事件
int readyCount = selector.select();
if(readyCount == 0) continue;
// 4.获取就绪事件集合
Set<SelectionKey> readyKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = readyKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
// 根据类型处理对应的 I/O 事件
if (key.isAcceptable()) {
// 处理接受请求事件
} else if (key.isConnectable()) {
// 处理连接事件
} else if (key.isReadable()) {
// 处理读事件
// ...读取数据
} else if (key.isWritable()) {
// 处理写事件
// ...发送数据
}
}
}
Selector 可以监听以下四种类型的事件:
SelectionKey.OP_ACCEPT: 表示 Channel 有新的客户端连接请求可以被接受,通常用于ServerSocketChannel。SelectionKey.OP_CONNECT: 表示 Channel 与服务器的连接已经建立成功, 通常用于SocketChannel。SelectionKey.OP_READ: 表示 Channel 准备进行读取的事件,即有数据可以读取。SelectionKey.OP_WRITE: 表示 Channel 准备好进行写入的事件,即可以写入数据。
一个 Selector 实例有三个 SelectionKey 集合:
- 所有的 SelectionKey 集合:代表所有注册在 Selector 上的 Channel。可以通过
keys()方法获取。 - 被选择的 SelectionKey 集合: 代表所有通过
select()方法获取的,需要进行 IO 处理的 Channel,可以通过selectedKeys()方法获取。 - 被取消的 SelectionKey 集合:代表所有被取消注册的 Channel,下次执行
select()时,这些 SelectionKey 会被彻底删除。
Selector 提供的一系列select()方法:
int select(): 阻塞监控所有的注册的 Channel,当它们中间有需要处理的 IO 操作时,方法返回,并将对应的 SelectionKey 加入到被选择的 SelectionKey 集合中。int select(long timeout): 可以设置超时时间的select()方法。int selectNow(): 无阻塞的select()方法,立即返回。int wakeup(): 使一个还未返回的 select() 方法立刻返回。
零拷贝技术?
零拷贝是提升 IO 操作性能的一个常用手段,像 ActiveMQ、Kafka、RocketMQ、Netty 等都用到了零拷贝技术。
零拷贝技术是指计算机在执行 IO 操作时, CPU 不需要将数据从一个存储区域复制到另一个存储区域,从而减少上下文切换以及 CPU 的拷贝时间。零拷贝的实现技术有:mmap+write、sendfile、sendfile+DMA gather copy。
Java 对零拷贝的支持:
MappedByteBuffer是 NIO 基于内存映射(mmap)这种零拷贝方式的一种实现,底层实际调用了 Linux 内核的 mmap 系统调用。它可以将一个文件或者文件的一部分映射到内存中,形成一个虚拟的内存文件,这样就可以直接操作内存中的数据,而不需要通过系统调用来读写文件。FileChannel的transferTo()/transferFrom()是 NIO 基于发送文件(sendfile)这种零拷贝方式的实现,底层调用了 Linux 内核的 sendfile 系统调用。它可以直接将文件数据从磁盘发送到网络,而不需要经过用户空间的缓冲区。