初学Java的时候大家都会接触到各种各样的IO流,我们可以知道,IO流的扩展方式是多种多样的,这篇文章,就会主要的来介绍一下IO流。
简介
Java IO流方式多种多样,我们可以从IO抽象模型和交互方式,进行简单的划分。
第一,传统的java.io包,完全基于流模型实现,提供了一些我们熟知的IO功能,比如File抽象、输入输出流等等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流的时候,在这两个操作结束之前,线程呈阻塞状态,但是呈线性,所以有一定规律。
java.io包的特点是,代码简单,原理移动,可扩展性低,性能差。
第二,为了在保证原本功能的情况下提升性能,所以java14引入了NIO框架,提供了Channel,Selector,Buffer等新的抽象类,构建多路复用,同步且非阻塞,且提高了性能。
第三,在java17中,NIO又进行了更新,也就是NIO2,引入了异步非阻塞IO,也就是我们听过的AIO(Asynchronous IO) 。
异步IO操作基于事件和回调,可以简单的理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成之后,操作系统会通知相应的线程在后续工作。
在面试中,一般面试方向是这样的:
- java.io的基础包用法,像Input/OutStream,Reader/Writer
- NIO和NIO2的基本组成
- 给定场景,用不同模型实现,分析BIO和NIO等模式的设计和实现原理。
- NIO提供的高性能数据操作方式是基于什么原理,如何使用
- NIO自身的问题,如何改进
细节上的东西在下一篇文章介绍,这篇文章简要的进行概述
扩展
首先区分一下同步和异步:
同步
:有序运行,后续任务等待当前调用返回,才会执行下一步
异步
:不需要等待当前调用返回,通常依赖事件、回调等机制实现任务间次序关系
然后区分一下阻塞与非阻塞:
阻塞
:当阻塞的时候,当前线程会处于阻塞状态,无法从事其他任务,只有当任务就绪才能继续,比如ServerSocket新连接建立完毕,或者数据读取,写入操作完成
非阻塞
:不管IO是否结束,直接返回,操作在后台自己处理。
不能一概而论,阻塞和同步就是低效,要看实际应用场景
对于java.io,我们都非常熟悉,但是还有很多底层和细节方面的问题需要仔细的探究,以下是必须要了解到的知识点:
- IO不仅仅是对文件的操作,更包括了Socket。
- 输入输出流是用于读取或写入字节的,例如操作图片文件。
- Reader/Writer 主要用于操作字符,增加了字符编解码等功能,适用于类似从文件中读取或者写入文本信息。本质上计算机操作的都是字符,不管是网络通信还是文件读取,Reader/Writer相当于构建了应用逻辑与原始数据之间的桥梁。
- BufferedOutputStream这种带缓冲区的IO读写可以大大提升读写的效率,但是最后需要flush
- 很多IO工具类都实现了Closeable接口,因为要进行资源的释放。比如,打开FileInputStream,它就会获取相应的文件描述符(FileDescriptor),需要利用try-with-resource、try-finally等机制保证FileInputStream被明确关闭,进而响应的文件描述符也会失效,否则资源将无法释放。
- Java NIO概述
首先,描述一下NIO的主要组成:
- Buffer :高效的数据容器,除了布尔类型,所有原始数据类型都有响应的Buffer来实现
- Channel:批量操作IO的抽象
- Selector:可以通过检测Channel的状态,达到单线程管理Channel
- Charset:提供Unicode字符串定义
- NIO能解决什么问题
我们要实现一个服务器应用,只简单要求能够同时服务多个客户端请求即可。
package jike_Time;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerNIO extends Thread{
private ServerSocket serverSocket;
public int getPort() {
return serverSocket.getLocalPort();
}
public void run() {
try {
serverSocket = new ServerSocket(0);
while (true) {
Socket socket = serverSocket.accept();
RequestHandler requestHandler = new RequestHandler(socket);
requestHandler.start();
}
} catch (Exception e) {
e.printStackTrace();
}finally {
if (serverSocket != null) {
try {
serverSocket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
ServerNIO serverNIO = new ServerNIO();
serverNIO.start();
try(Socket client = new Socket(InetAddress.getLocalHost(), serverNIO.getPort())){
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
bufferedReader.lines().forEach(s -> System.out.println(s));
}
}
}
//简化实现
class RequestHandler extends Thread {
private Socket socket;
RequestHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (PrintWriter out = new PrintWriter(socket.getOutputStream())) {
out.print("Hello world");
out.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
其实现要点在于:
- 服务器端启动ServerSocket,端口0表示总动绑定一个空闲端口
- 调用accept方法,阻塞等待客户端连接
- 利用Socket模拟了一个简单的客户端,只连接、读取、打印
- 当连接建立之后,启动一个单线程负责恢复客户端请求
这样一个Socket服务器就实现了,那么会存在什么潜在的问题呢?
我们都知道java的线程是比较重量级的,所以启动,关闭,销毁一个进程是有一定开销的,每一个线程都有单独的线程栈,需要很明显的内存支持,所以我们可以引入线程池。
ExecutorService executorService = Executors.newFixedThreadPool(8);
这是一个经典的并发服务,可以参考下图来看:
当我们用普通IO进行这种低并发的时候,效果还是可以的,但是当连接数量急剧上升的时候,就会出现同步阻塞的低扩展劣势,而NIO解决了这样的问题。
- 首先,通过Selector.open()创建一个Selector,作为类似调度员的角色。
- 然后创建一个ServerSocketChannel,并且向Selector注册,通过指定SeletionKey.OP_ACCEPT,告诉调度员,它关注的是新的连接请求。
注意,为什么我们要明确配置非阻塞模式呢?这是因为阻塞模式下,注册操作是不允许的,会抛出异常 - Selector阻塞在select操作,当有Channel发生接入请求,就会被唤醒。
- 在数据的方法中,通过SocketChannel和Buffer进行数据操作,在本例中是发送了一串String。
我们可以得出以下结论:
IO都是同步阻塞,所以需要多线程,NIO则是单线程轮询,提高应用的扩展能力。