IO通信即消息的输入、输出,这涉及到应用、操作系统以及硬件之间的通信,在java中,这涉及到用户态、内核态以及硬件的操作。在jdk1.4之前,IO(统称BIO)通信在Java语言中,性能一直堪忧,基于此,在jdk1.4开始,NIO面世了,并以高性能而闻名,并以此为基础,做了许多结构优化。
一、Java IO 模型
1、同步 VS 异步
同步:每个请求必须逐个的被处理,在上一个请求未执行完之前,下一个请求无法执行。是一种串行的处理模式。在Java语言中,用户线程发起IO操作,必须等待内核IO处理完之后,用户线程才能继续后续工作。
异步:多个请求可以并行工作,每个请求的工作都相对是独立的,不会相互作用。在Java语言中,用户线程发起IO操作,不用等待内核IO的操作,可以直接处理后续工作,等内核IO处理完后,后通知用户线程。例如:线程回调的方式。
2、阻塞 VS 非阻塞
阻塞:某个请求发出后,如果请求需要的条件无法得到满足,则后一直处于等待状态。
非阻塞:某个请求发出后,如果请求需要的资源无法得到满足,也会立即收到结果,告知不满足,不会一直处于等待状态,后面可以通过轮询的方式来判断请求需要的条件是否已经可以满足,并能获取最终结果。
PS:阻塞不等于同步,非阻塞不等于异步,因为侧重点不同。阻塞/非阻塞的侧重点是,请求是否立刻返回,即使返回的是条件不足。而同步/异步的侧重点是,多个请求时,后发的请求是否必须等待先发的请求完成,才能进行后续处理。
2、BIO
BIO就是同步阻塞的IO操作。下图是BIO在进行IO操作时,数据流向过程。
User Space是用户态空间,Kernel Space是内核态空间,Disk是硬盘。
在读取硬盘文件时,首先文件数据是从Disk复制到Kernel Space的buffer,再从Kernel Space的buffer复制到User Space的buffer,最后Java代码操作的是User Space的buffer。
总结:通过BIO,读取硬盘的文件数据,要经历两次复制的操作,才能把数据读取到内存空间。
3、NIO
NIO是同步非阻塞的IO操作。下图是NIO在进行IO操作时,数据流向过程。
User Space是用户态空间,Kernel Space是内核态空间,Disk是硬盘。
在读取硬盘文件时,首先文件数据是从Disk复制到Kernel Space的buffer,该buffer的地址会映射到Physical memory(Direct Memory)中(可以自行查阅内存映射缓冲区),Java代码通过地址映射能直接读取到Kernel Space的buffer中数据。
总结:通过NIO,读取硬盘的文件数据,最多经历一次复制,就能把数据读取到内存中。
4、BIO/NIO文件拷贝测试
在本地同时拷贝一份400M的mp4资源,BIO跟NIO所消耗的时间比较。
public static void main(String[] args) throws Exception {
String sourcePath = "/Users/Desktop/IO.mp4";
String bioTarget = "/Users/Desktop//IO.bio.mp4";
String nioTarge = "/Users/Desktop/mytest/课件/IO/IO.nio.mp4";
long t1 = System.currentTimeMillis();
bioCopy(sourcePath, bioTarget);
long t2 = System.currentTimeMillis();
System.out.println("BIO实现文件拷贝耗时:" + (t2-t1) + "ms");
nioCopy(sourcePath, nioTarge);
long t3 = System.currentTimeMillis();
System.out.println("NIO实现文件拷贝耗时:" + (t3-t2) + "ms");
}
private static void bioCopy(String sourcePath, String destPath) throws Exception{
File source = new File(sourcePath);
File dest = new File(destPath);
if(!dest.exists()) {
dest.createNewFile();
}
FileInputStream fis = new FileInputStream(source);
FileOutputStream fos = new FileOutputStream(dest);
byte [] buf = new byte [1024];
int len = 0;
while((len = fis.read(buf)) != -1) {
fos.write(buf, 0, len);
}
fis.close();
fos.close();
}
private static void nioCopy(String sourcePath, String destPath) throws Exception{
File source = new File(sourcePath);
File dest = new File(destPath);
if(!dest.exists()) {
dest.createNewFile();
}
FileInputStream fis = new FileInputStream(source);
FileOutputStream fos = new FileOutputStream(dest);
FileChannel sourceCh = fis.getChannel();
FileChannel destCh = fos.getChannel();
destCh.transferFrom(sourceCh, 0, sourceCh.size());
sourceCh.close();
destCh.close();
}
5、IO在tomcat中优化。
在tomcat7以及之前,tomcat默认都是以BIO的方式进行启动的,在tomcat8开始,都是默认以NIO的方式开始进行启动。
下图是tomcat/config/server.xml中关于8080的配置。
tomcat7跟tomcat8的配置是一样的,但是在启动日志里面,会看出不一样的情况,请看下图:
tomcat7的启动日志:能清晰的看到bio字眼
tomcat8的启动日志:能清晰的看到nio字眼
tomcat7跟tomcat8在server.xml中关于8080的配置是一样的,但是启动日志不一样,在这里说明tomcat用nio的方式比用bio的方式,性能更加的强大。下面是作者用jmeter给予bio/nio做的压测。
以800线程做的测试:
以1000线程做的测试:
以1200线程做的测试:
因为考虑的不确定的因素可能会对测试结果造成影响,所以作者特地测试了三次,从结果来看,在tomcat中,使用nio后的性能确实比bio的性能好。
6、NIO在Redis中使用。
百度:Redis是单线程,还是多线程
得到的结论是:Redis是单线程的。
那既然Redis是单线程的,为什么确是高并发优化中必不可少的一个组件呢?还能支撑几万的QPS呢。
之所以说Redis是单线程的,是因为Redis是用单线程来处理所有的客户端request请求。但Redis的后台任务,还是多线程的模式在进行的。单线程就能处于所有的客户端request请求,是基于一种epoll模型进行工作的。epoll的伪代码如下:
从结构上看,等同于Java NIO中的Selector。
下面演示NIO在socket.io通信中优化。
客户端code:
将hello做延迟发送,一次发送一次字符。
客户端时间消耗:
BIO服务端code:
NIO服务端code:
总结:
客户端,因为发送数据做了延迟操作,所消耗的时间都是6000ms。
BIO服务端,从接收客户端消息到接收完,每条都要耗时6000ms。
NIO服务端,从接收客户端消息到接收完,每条耗时只要几毫秒。
对于客户端来说,时间性能上没有提升。但是对于服务端来说,使用NIO比BIO在时间性能上提高了几千倍。
所以:BIO是阻塞的,而NIO是非阻塞的。NIO可以在BIO阻塞的时间上去处理别的操作,从而提高服务端性能。
7、IO模型图
BIO模型图:
NIO模型图: