字符流
字节流主要是指InputStream和OutputStream以及其各种子类。它们操作的单位是字节(byte),能够读取或写入任意类型的数据,包括二进制数据,例如图像、音频和视频等。字节流可以通过InputStream和OutputStream的各种子类进行实例化,例如FileInputStream、FileOutputStream、ByteArrayInputStream、ByteArrayOutputStream等。
以下是一个示例,演示如何使用字节流从文件中读取数据并输出到控制台:
javaCopy code
import java.io.*;
public class ByteStreamDemo {
public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream("file.txt");
int data;
while ((data = fis.read()) != -1) {
System.out.print((char) data);
}
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
字节流
字符流主要是指Reader和Writer以及其各种子类。它们操作的单位是字符(char),能够读取或写入文本数据。字符流可以通过Reader和Writer的各种子类进行实例化,例如FileReader、FileWriter、CharArrayReader、CharArrayWriter等。
以下是一个示例,演示如何使用字符流从文件中读取数据并输出到控制台:
javaCopy code
import java.io.*;
public class CharStreamDemo {
public static void main(String[] args) {
try {
FileReader fr = new FileReader("file.txt");
int data;
while ((data = fr.read()) != -1) {
System.out.print((char) data);
}
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
注意,字符流是基于字节流构建的。当我们使用字符流进行读写操作时,实际上是将字节流转换为字符流,这个过程叫做编码(Encoding)和解码(Decoding)。编码过程将字符转换为字节,解码过程将字节转换为字符。在Java中,默认的编码方式是UTF-8。
一般来说,如果是处理文本数据,使用字符流会更加方便和安全。因为字符流会自动处理编码和解码的过程,避免了在处理文本数据时出现乱码的情况。如果是处理二进制数据,使用字节流则更加合适。
在处理二进制数据时,使用字节流是更好的选择。字节流可以读写任何类型的数据,包括二进制数据。使用字节流处理二进制数据,可以确保数据的完整性和准确性。
如果使用字符流处理二进制数据,可能会出现数据被破坏或丢失的问题。
输入流
输入流(InputStream)是用来从数据源(例如文件、网络、标准输入等)读取数据的抽象类,它提供了一些基本的方法来读取字节或字符流。InputStream是一个抽象类,Java中有许多具体的输入流类实现了该类。例如,FileInputStream用于从文件中读取数据,ByteArrayInputStream用于从字节数组中读取数据,ObjectInputStream用于从序列化对象中读取数据。
以下是一个示例,演示如何使用FileInputStream从文件中读取数据:
javaCopy code
import java.io.*;
public class InputStreamDemo {
public static void main(String[] args) {
try {
InputStream is = new FileInputStream("file.txt");
int data;
while ((data = is.read()) != -1) {
System.out.print((char) data);
}
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出流
输出流(OutputStream)是用来向数据目标(例如文件、网络、标准输出等)写入数据的抽象类,它提供了一些基本的方法来写入字节或字符流。OutputStream是一个抽象类,Java中有许多具体的输出流类实现了该类。例如,FileOutputStream用于将数据写入文件,ByteArrayOutputStream用于将数据写入字节数组,ObjectOutputStream用于将对象序列化并写入输出流。
以下是一个示例,演示如何使用FileOutputStream将数据写入文件:
javaCopy code
import java.io.*;
public class OutputStreamDemo {
public static void main(String[] args) {
try {
OutputStream os = new FileOutputStream("file.txt");
String message = "Hello, World!";
byte[] data = message.getBytes();
os.write(data);
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
注意,使用输出流写入数据时,要注意缓冲和刷新的问题。OutputStream提供了一个flush()方法,可以强制将所有的缓冲数据写入输出流中。在使用OutputStream写入数据时,如果数据不是非常紧急,可以使用缓冲区来提高性能。BufferedOutputStream是一个具有缓冲功能的输出流,它将数据写入缓冲区中,等到缓冲区满了或者调用flush()方法时,才将数据写入输出流中。
这里的输入、输出的参考坐标是程序本身,设想一下自己就是那个读写程序,如果需要从外部读取数据,那就是输入流,如果是需要向外部写出数据,那就是输出流。
注意事项
所有IO涉及到的流,都需要有一些注意事项:
- 关闭流:使用完流之后,一定要关闭流。这可以使用close()方法来完成。如果不关闭流,会导致资源泄露,可能会引起程序运行出错。
- 异常处理:流操作可能会抛出异常,例如IOException。因此,在使用流时,要在try-catch语句块中进行异常处理,以保证程序的健壮性和可靠性。
- 缓冲和刷新:当使用输出流时,需要注意缓冲和刷新的问题。OutputStream提供了一个flush()方法,可以强制将所有的缓冲数据写入输出流中。一定要注意及时刷新缓冲区,避免出现数据在写出结束后仍旧有数据停留在缓冲区,导致数据部分缺失。
- 转换流:当处理字符数据时,可以使用转换流将字节流转换为字符流。InputStreamReader和OutputStreamWriter是Java提供的两个转换流,它们分别将字节流转换为字符流和将字符流转换为字节流。这些转换流可以处理字符编码问题,确保不同编码的数据可以正确读写。
- 先开后关:在程序中,一般按照先开先关的原则来操作流。也就是说,如果使用多个流,应该按照它们的顺序依次打开,并按照相反的顺序依次关闭。这样可以避免资源占用过多的问题。
同步、异步、阻塞、非阻塞
同步 VS 异步
这两个概念通常在Java的线程中使用较多:
同步是指在程序中对共享数据的访问具有序列化的特性,比如:对于多个线程同时访问同一块数据时,同一时间只允许一个线程访问,其他线程则需要等待当前线程访问完毕后再进行访问。
而异步则是指程序的执行方式是不连续的,即当程序发出某个请求后,不需要等待结果返回就可以进行下一步操作,结果返回时再通知程序进行处理。在 Java 中,可以使用回调函数、Future 等方式来实现异步编程。
举个例子:比如两个人共用一个电瓶车,这时候如果A骑走了,在同步模型下,B就一直等A用完后骑回来,期间什么也不干,A回来后再B再接着用;而异步模型下,A骑走了,B就去干其他事情了,比如跑附近的公园溜达了一会儿,等A回来后再跑回来骑电瓶车。
阻塞 VS 非阻塞
所谓阻塞,就是指在Java中进行某项操作时,阻塞了当前正在运行的线程,必须等待结果反馈出来才进行后续操作,比如常见的控制台等待用户输入就属于阻塞在那里场景,主线程一直等待输入事件,卡住不动。
非阻塞就是在进行某项比较费时的操作时,允许线程先去干其他的事情,等到该操作完成后再唤回线程继续干后面的事情,它不会将当前线程一直挂起,线程可以去在这个空闲阶段去忙其他的事情。
简单比喻来说:比如你想烧水泡方便面,在这个场景中,如果是阻塞模型,则你就是先烧水,然后就在水壶边等待水开后再泡面,期间不会干其他事情。如果是异步模型,先烧水,在水正在烧的过程中你去打了把游戏,等到水壶响,你再跑过去把方便面泡掉,这个模型里,烧水阶段并不强制要求你必须在旁边等待,你可以去干其他事情,等到水开后会有个提醒,然后再回过来去泡面。
猛然间一看好像同步和阻塞、异步和非阻塞是两组相似的概念;但是实际上仔细品味下就可以发现,对于同步和异步更多的是关注于程序与程序之间或者说操作与操作之间的一种交互方式;阻塞和非阻塞更多的是关注于操作过程中的等待状态;
两者之间是彼此独立的概念,没有非此即彼的说法,这话如何说起呢?比如说:确实是存在这样一种情况的--同步非阻塞、异步阻塞这类的概念,这主要跟具体的实现方式有关,那该如何理解这种场景呢?
同步非阻塞
同步非阻塞是指在进行某个操作的时候,程序会立即返回,不会被阻塞,但是在操作完成之前需要不断地轮询操作状态,直到操作完成后再进行后续处理。在同步非阻塞模型中,程序需要不断地检查操作的状态,直到操作完成,然后再进行下一步操作,这个过程称为轮询或者自旋。同步非阻塞常常与多线程编程和网络编程中的事件循环机制相结合使用。
在一些频繁出现不太耗时的操作场景下,同步非阻塞的优点是可以提高程序的响应速度,不会浪费CPU资源。由于在操作完成之前程序不会被阻塞,所以可以更好地利用CPU资源,提高程序的吞吐量和并发能力。同时,同步非阻塞还可以避免死锁等问题,提高程序的稳定性和可靠性。
然而,同步非阻塞也存在一些缺点。由于需要不断地轮询操作状态,所以会增加CPU的负担,可能会降低程序的性能。此外,在使用同步非阻塞模型时,程序需要处理轮询操作的逻辑,可能会增加程序的复杂性和代码量。
异步阻塞
异步阻塞在实际场景中出现的情况就很少了,因为一般使用异步就是为了非阻塞,但是在一些特殊场合下还是会存在这种情况的:
- 需要执行同步代码:如果异步操作需要调用同步代码,而该代码是必要的,则可能需要使用异步阻塞方式。在这种情况下,异步操作将阻塞程序的执行,直到同步代码执行完成。
- 操作过程非常短暂:如果异步操作的执行时间非常短暂,而且对程序的性能没有明显的影响,那么使用异步阻塞方式可能是可以接受的。
总之,使用异步阻塞方式的情况应该尽可能少,只有在确实需要时才应该使用,并且应该权衡其优缺点,确保其使用不会对程序的性能和可维护性造成不良影响。
linux的IO模型
- 阻塞式 IO:程序调用 IO 操作时,如果没有准备好数据或者无法立即完成,IO 操作就会阻塞程序,程序会一直等待,直到 IO 操作完成。在阻塞 IO 模型中,程序需要等待 IO 操作完成后才能继续执行。
- 非阻塞式 IO:程序调用 IO 操作时,如果没有准备好数据或者无法立即完成,IO 操作就会立即返回一个错误码,表示操作无法完成。在非阻塞 IO 模型中,程序需要不断地轮询 IO 状态,直到数据准备好或者 IO 操作完成,才能继续执行。
- I/O 多路复用:在 I/O 多路复用模型中,程序使用一个系统调用同时监视多个 IO 操作的状态,一旦有任何一个 IO 操作准备好数据,程序就可以立即响应,而无需等待其他操作。常见的 I/O 多路复用技术包括 select、poll 和 epoll。
- 信号驱动 IO:在信号驱动 IO 模型中,程序使用信号来通知程序有 IO 操作准备好数据,当有数据可读时,内核会向程序发送一个信号,程序收到信号后就可以执行 IO 操作。
- 异步 IO:在异步 IO 模型中,程序发起一个 IO 操作后,就可以继续执行其他操作,不需要等待 IO 操作完成。当 IO 操作完成后,内核会向程序发送一个通知,程序就可以处理 IO 操作的结果。异步 IO 模型通常需要操作系统和硬件的支持,比较复杂,但可以实现高效的 IO 操作。常见的异步 IO 技术包括 AIO 和 io_uring。
每种 IO 模型都有其特点和适用场景,可以根据实际情况选择适合自己的 IO 模型。
BIO、NIO、AIO
- 同步阻塞 IO(Blocking IO,BIO):也叫传统 IO,是最早的 IO 实现方式。在 BIO 中,IO 操作是同步的,并且在 IO 操作完成之前会一直阻塞程序的执行。这意味着程序需要等待 IO 操作完成才能继续执行后面的代码。BIO 的优点是实现简单,容易理解,但其缺点是无法同时处理多个连接,每个连接都需要一个线程来处理,线程数量过多会导致系统资源浪费和效率低下。(BIO 的用法非常简单,就是使用 Java 中的传统 IO API(如 InputStream 和 OutputStream)进行文件或网络 IO 操作。)
- 同步非阻塞 IO(Non-Blocking IO,NIO):也叫 NIO,是相对于传统 IO 的一种新型 IO 模式。在 NIO 中,IO 操作是非阻塞的,程序可以不断轮询 IO 操作的状态,而不会阻塞程序的执行。这意味着程序可以同时处理多个连接,使用较少的线程就能处理大量的连接。但是,NIO 的实现比较复杂,编程难度较大。(NIO 的用法相对复杂一些,需要使用 Java 中的 NIO API(如 Selector、SocketChannel 和 ByteBuffer)进行文件或网络 IO 操作。NIO 的原理是利用一个 Selector 管理多个 Channel,通过轮询 Selector 管理的 Channel,实现 IO 操作的非阻塞处理。)
- 异步非阻塞 IO(Asynchronous IO,AIO):也叫 AIO,是一种基于事件驱动的 IO 模式。在 AIO 中,IO 操作是异步的,并且不会阻塞程序的执行。程序可以发起一个 IO 请求后,就可以继续执行其他操作,当 IO 操作完成后,系统会向程序发送通知,程序再去处理 IO 操作的结果。AIO 模式下,不需要使用轮询来等待 IO 操作完成,因此可以提高系统的吞吐量和性能。但是,AIO 的实现比 NIO 还要复杂,编程难度更大。(AIO 的用法和 NIO 相似,也需要使用 Java 中的 AIO API 进行文件或网络 IO 操作。AIO 的原理是利用操作系统内核的异步 IO 接口,通过操作系统内核实现 IO 操作的非阻塞处理。)
综上所述,BIO 适用于连接数较少的情况,NIO 适用于连接数较多的情况,而 AIO 则适用于处理大量的小数据请求。在实际应用中,我们需要根据不同的业务需求,选择合适的 IO 模式来进行开发。
一个常见的使用 AIO 的场景是网络编程中的异步 IO 操作,例如服务器需要处理大量的客户端连接请求,并且每个客户端连接请求可能会包含多个数据请求。在这种情况下,使用 AIO 可以大大提高服务器的并发性能和处理能力。
以下是一个简单的使用 AIO 进行网络编程的例子:
javaCopy code
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
public class AioServer {
public static void main(String[] args) throws Exception {
final AsynchronousServerSocketChannel listener = AsynchronousServerSocketChannel.open()
.bind(new InetSocketAddress("localhost", 8080));
listener.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
public void completed(AsynchronousSocketChannel ch, Void att) {
// 处理连接成功的客户端 SocketChannel
listener.accept(null, this);
ByteBuffer buffer = ByteBuffer.allocate(1024);
ch.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
public void completed(Integer result, ByteBuffer attachment) {
// 处理读取成功的数据
attachment.flip();
byte[] data = new byte[attachment.limit()];
attachment.get(data);
System.out.println(new String(data));
attachment.clear();
ch.write(ByteBuffer.wrap("Hello, Client".getBytes()), attachment, this);
}
public void failed(Throwable exc, ByteBuffer attachment) {
// 处理读取失败的情况
}
});
}
public void failed(Throwable exc, Void att) {
// 处理连接失败的情况
}
});
// 进入等待状态,等待客户端连接
Thread.currentThread().join();
}
}
在这个例子中,我们使用了 Java 的 AIO 框架来创建一个简单的服务器,监听 8080 端口的客户端连接请求。当客户端连接成功后,我们使用 AIO 框架提供的异步 IO 操作接口,处理客户端连接的读写请求。在读取客户端数据成功后,我们将 Hello, Client 这个字符串作为响应写回客户端。
需要注意的是,在使用 AIO 进行异步 IO 操作时,程序需要使用回调函数的方式处理 IO 操作的结果。这使得代码实现稍微有些复杂,但同时也带来了高并发处理的好处。
Netty中的IO
Netty 是一个基于 NIO 的高性能网络编程框架,它在 NIO 的基础上做了很多优化和改进,提供了更加易用和高效的 API,而且可以支持多种不同的 IO 模型。
Netty 支持以下三种 IO 模型:
1. NIO
Netty 默认采用 NIO 模型作为 IO 模型。NIO 模型是基于事件驱动的非阻塞 IO 模型,它通过 Selector 进行事件轮询,当某个 Channel 可以进行 IO 操作时,就会通知对应的 Channel 进行 IO 操作。
2. Epoll
Epoll 是 Linux 下的一种高性能的 IO 模型,它可以支持更高的并发连接数和更快的 IO 处理速度。Netty 可以在 Linux 系统上使用 Epoll 模型,从而获得更高的性能和更好的响应速度。
3. OIO
OIO(Old-IO)模型是 Netty 中最基础的 IO 模型,也就是传统的阻塞 IO 模型。OIO 模型在 Netty 中主要用于兼容老的系统和协议。
总的来说,Netty 的 IO 模型非常灵活,可以根据不同的应用场景和操作系统选择最适合的 IO 模型,从而获得更好的性能和更高的吞吐量。同时,Netty 还提供了很多高级的特性和组件,如线程池、编解码器、心跳机制等,能够帮助开发者更加方便地实现网络编程功能。