Java中的IO原理
首先Java中的IO都是依赖操作系统内核进行的,我们程序中的IO读写其实调用的是操作系统内核中的read&write两大系统调用。
操作系统内核是如何进行IO交互的呢?
- 网卡中的收到经过网线传来的网络数据,并将网络数据写到内存中。
- 当网卡把数据写入到内存后,网卡向cpu发出一个中断信号,操作系统便能得知有新数据到来。
- 通过网卡中断程序去处理数据。将内存中的网络数据写入到对应socket的接收缓冲区中。
- 当接收缓冲区的数据写好之后,应用程序开始进行数据处理。
- 处理完毕,释放相关资源(释放socket的缓存输入流)。
JAVA中的IO和OS中的IO很像,而BIO、NIO、AIO之间的区别就在于这些操作是同步还是异步,阻塞还是非阻塞。
同步与异步
案例代码
public class Test {
//这里假设a方法业务复杂,执行需要耗费3秒的世时间
public static void a(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("testA");
}
public static void b(){
System.out.println("testB");
}
public static void main(String[] args) {
a();
b();
}
}
同步指的是调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为,参考如上main方法中的执行情况,b方法必须等a方法执行完毕才能执行,所以此时a和b是同步执行的
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
a();
}
});
b();
}
异步指的是调用立刻返回,调用者不必等待方法内的代码执行结束,就可以继续后续的行为,通常是额外开始一个线程去执行a方法(方法执行完毕可能会涉及到回调),b方法则依然是main方法的主线在处理,这种情况下方法a不必等到方法b完成即可开始执行,所以a和b是异步执行的
开发经验
通常在业务需求中,当上下文内容没有关联的时候,上一个操作比较耗时(例如a方法), 我们无需等待上一个执行结束才开始下一个执行,本质就是为了解决主线程的阻塞,尽可能提高接口的响应速度
阻塞与非阻塞
阻塞与非阻塞的区别主要是单个线程内遇到同步等待时,是否在原地不做任何操作。
- 阻塞指的是遇到同步等待后,一直在原地等待同步方法处理完成,即每一个请求都要对应一个专门的线程去处理,例如上面的代码server.accept();
- 非阻塞指的是遇到同步等待,不在原地等待,先去做其他的操作,隔断时间再来观察同步方法是否完成,往往是一个线程跟踪多个 socket 状态,哪个socket就绪了线程就去操作哪个socket。
BIO通信模型图
概念
BIO本身是身是同步阻塞模式,线程发起IO请求后,一直阻塞IO,直到相关请求处理完毕后,网络通信模型都是一请求一应答的方式,虽然简化了上层的应用开发,但在性能和可靠性方面存在着巨大瓶颈,试想一下如果每个请求都需要新建一个线程来专门处理,那么在高并发的场景下,机器资源很快就会被耗尽
BIO架构模型
代码模拟
public class BIOServer {
public static void main(String[] args) throws Exception {
//线程池机制
//思路
//1. 创建一个线程池
//2. 如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
//创建ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器启动了");
while (true) {
System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
//监听,等待客户端连接
System.out.println("等待连接....");
//会阻塞在accept()
final Socket socket = serverSocket.accept();
System.out.println("连接到一个客户端");
//就创建一个线程,与之通讯(单独写一个方法)
newCachedThreadPool.execute(new Runnable() {
public void run() {//我们重写
//可以和客户端通讯
handler(socket);
}
});
}
}
//编写一个handler方法,和客户端通讯
public static void handler(Socket socket) {
try {
System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
byte[] bytes = new byte[1024];
//通过socket获取输入流
InputStream inputStream = socket.getInputStream();
//循环的读取客户端发送的数据
while (true) {
System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
System.out.println("read....");
int read = inputStream.read(bytes);
if (read != -1) {
System.out.println(new String(bytes, 0, read));//输出客户端发送的数据
} else {
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("关闭和client的连接");
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
打开cmd用来模拟一个本地客户端进程,相关命令如下
telnet 127.0.0.1 6666 <!-- 和BIOServer建立通信连接 ->
send 'ok' <!-- 向客户端发送消息 ->
服务器端代码中日志打印
服务器启动了
线程信息id = 1名字 = main
等待连接....
连接到一个客户端
线程信息id = 1名字 = main
等待连接....
线程信息id = 12名字 = pool-1-thread-1
线程信息id = 12名字 = pool-1-thread-1
线程信息id = 12名字 = pool-1-thread-1
read....
线程信息id = 12名字 = pool-1-thread-1
read....
线程信息id = 12名字 = pool-1-thread-1
read....
'ok' <!-- 我们客户端测试的那条消息 ->
线程信息id = 12名字 = pool-1-thread-1
read....
线程信息id = 12名字 = pool-1-thread-1
read....
线程信息id = 12名字 = pool-1-thread-1
read....
同理,打开多个cmd模拟多个客户端进程和服务器端建立并保持连接,你们会发现每个客户端进程都会服务器端都会从线程池中取出不同的线程单独处理处理这个连接,负责这个连接的读read写write操作的完成
经过测试,我们可以很容易看到BIO和弊端
- 客户端请求和线程之间是总是一对一的关系,那意味着客户端连接即便是没有任何读写操作,这个线程的工作就很闲,造成线程资源的浪费
- 如果是高并发情况,需要使用大量线程去完成处理,浪费大量线程资源,这种架构设计是不适用于处理高并发情况的
NIO
同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理,当前连接没有请求,那么线程则切换到其它有读写操作的连接去处理,线程和客户端连接请求间不需要一一再对应,一个线程可以同时处理多个客户的连接的请求,NIO 是面向缓冲区,或者面向块编
NIO架构模型
NIO 全称 Java non-blocking IO即非阻塞型IO
NIO 有三大核心部分
- 缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块
- Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer
- Selector(选择器):用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
Channel和Buffer的关系
Channel和Buffer是一一对应的,如果说BIO基于字节流和字符流进行操作,那么NIO则是基于Channel和Buffer缓冲块进线操作的,Channel和Buffer是双向的,数据总是由Buffer写入Channel,或者从Channel中读取到Buffer
图解NIO三大核心组件
NIO案例代码
public class NIOFileChannel03 {
public static void main(String[] args) throws Exception {
FileInputStream fileInputStream = new FileInputStream("1.txt");
FileChannel fileChannel01 = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
FileChannel fileChannel02 = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
while (true) { //循环读取
//这里有一个重要的操作,一定不要忘了
/*
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
*/
byteBuffer.clear(); //清空 buffer
int read = fileChannel01.read(byteBuffer);
System.out.println("read = " + read);
if (read == -1) { //表示读完
break;
}
//将 buffer 中的数据写入到 fileChannel02--2.txt
byteBuffer.flip();
fileChannel02.write(byteBuffer);
}
//关闭相关的流
fileInputStream.close();
fileOutputStream.close();
}
}