NIO概览
每篇一句
悲伤能够给人力量,但是感恩更能给人带来希望
一、NIO简介
Java NIO是java 1.4之后新出的一套IO接口,这里的新是相对于原由标准的Java IO接口
NIO中的N可以理解为Non-blocking,不是单纯的New
NIO是面向缓冲并且基于通道的IO操作方法,在JDK7中NIO得到了扩展,为文件系统功能和文件处理做了增强
NIO被广泛应用于文件处理,现在常用的Netty框架底层就是基于NIO
二、NIO与IO的区别
IO是面向流,NIO是面向缓冲区
- 标准的IO编程是面向字节流和字符流的;而NIO是面向通道和缓冲区,数据总是从通道中读到buffer缓冲区内,或者从buffer缓冲区写入到通道中(NIO中所有的IO操作都是通过通道来完成的)
- Java IO每次从流中读取一个或多个字节直到读取完所有字节,整个过程没有被缓存在任何地方
- Java NIO会将数据读到缓冲区,然后再使用通道进一步处理数据
IO是阻塞的,NIO是不阻塞
- Java IO场景下,当一个线程调用read()/write()时,这个线程会被阻塞直到数据完全读取/写入,这个线程在这个过程中不能做任何事
- Java NIO场景下,当一个线程读取/写入数据到buffer时,它可以继续做其他的事,然后再回来处理buffer中的数据
NIO有选择器,而IO没有
- 选择器使单个线程可以处理多个Channel,由于线程之间的切换对于操作系统来说是昂贵的,因此选择器可以提高系统效率
三、NIO核心组件介绍
NIO包含下面几个核心的组件:Channel、Buffer和Selector
Channel
在Java NIO中,主要使用的通道如下(涵盖了UDP 和 TCP 网络IO,以及文件IO):
- DatagramChannel
- SocketChannel
- FileChannel
- ServerSocketChannel
Buffer
在Java NIO中使用的核心缓冲区如下(覆盖了通过I/O发送的基本数据类型:byte, char、short, int, long, float, double ,long):
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- FloatBuffer
- DoubleBuffer
- LongBuffer
Selector
Java NIO提供Selector是用于监视多个Channel的对象。如数据到达、连接打开等等
使用Selector时需要先将Channel注册到Selector上,然后调用Selector的select()方法。这个方法会进入阻塞直到有一个Channel的状态符合条件
四、IO读取性能比较
public class Main {
public static void main(String[] args) {
String filePath = "D:\\testData.txt";
String newPath = "D:\\testData_output.txt";
try {
File file = new File(filePath);
if(!file.exists()){
return;
}
File newFile = new File(newPath);
//randomAccessRead(file ,newFile);
//测试:读取1.5GB文件耗时:7721ms
//bufferedRead(file ,newFile);
//测试:读取1.5GB文件耗时:1975ms
//scannerRead(file ,newFile);
//测试:读取1.5GB文件耗时:115550ms
//fileChannelRead(file ,newFile);
//1.5GB文件
//每次读取1M,耗时:7935ms
//每次读取5M,耗时:2816ms
//每次读取10M,耗时:1967ms
//每次读取20M,耗时:1384ms
//每次读取30M,耗时:1218ms
//每次读取40M,耗时:1134ms
//每次读取50M,耗时:1108ms
//每次读取100M,耗时:976ms
//每次读取500M,耗时:920ms
fileChannelDirectRead(file, newFile);
//1.5GB文件
//每次读取1M,耗时:7756ms
//每次读取5M,耗时:2507ms
//每次读取10M,耗时:1846ms
//每次读取20M,耗时:1200ms
//每次读取30M,耗时:1150ms
//每次读取40M,耗时:969ms
//每次读取50M,耗时:939ms
//每次读取100M,耗时:896ms
//每次读取500M,耗时:769ms
} catch (Exception e) {
e.printStackTrace();
}
}
public static void randomAccessRead(File file , File newFile){
//测试:读取1.23GB文件耗时:8768ms
long d1 = System.currentTimeMillis();
RandomAccessFile raf = null;
OutputStream output = null;
try {
raf = new RandomAccessFile(file , "rw");
output = new FileOutputStream(newFile);
int len = 0; //每次读取内容长度
byte[] data = new byte[1024];//内容缓冲区
while((len = raf.read(data)) != -1){
output.write(data, 0, len);
}
long d2 = System.currentTimeMillis();
System.out.println("randomAccessRead读取完成,耗时:" + (d2 - d1));
} catch (Exception e) {
e.printStackTrace();
}finally{
try {
if(raf != null){
raf.close();
}
if(output != null){
output.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 使用NIO的FileChannel读取
* @param file
* @param newFile
*/
public static void fileChannelRead(File file , File newFile){
//1.5GB文件
//每次读取1M,耗时:7935ms
//每次读取5M,耗时:2816ms
//每次读取10M,耗时:1967ms
//每次读取20M,耗时:1384ms
//每次读取30M,耗时:1218ms
//每次读取40M,耗时:1134ms
//每次读取50M,耗时:1108ms
//每次读取100M,耗时:976ms
//每次读取500M,耗时:920ms
long d1 = System.currentTimeMillis();
FileInputStream in = null;
FileOutputStream output = null;
FileChannel fic = null;
FileChannel foc = null;
try {
in = new FileInputStream(file);
output = new FileOutputStream(newFile);
fic = in.getChannel();
foc = output.getChannel();
//fic.transferTo(0, fic.size(), foc);
ByteBuffer buf = ByteBuffer.allocate(1024*500);
while(fic.read(buf) != -1){
buf.flip();//切换到读取数据模式
foc.write(buf);//将缓冲区的数据写入通道中
buf.clear();//清空缓冲区
}
long d2 = System.currentTimeMillis();
System.out.println("fileChannelRead读取完成,耗时:" + (d2 - d1));
} catch (Exception e) {
e.printStackTrace();
}finally{
try {
if(in != null){
in.close();
}
if(output != null){
output.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 使用NIO的FileChannel读取(堆外内存)
* @param file
* @param newFile
*/
public static void fileChannelDirectRead(File file , File newFile){
//1.5GB文件
//每次读取1M,耗时:7756ms
//每次读取5M,耗时:2507ms
//每次读取10M,耗时:1846ms
//每次读取20M,耗时:1200ms
//每次读取30M,耗时:1150ms
//每次读取40M,耗时:969ms
//每次读取50M,耗时:939ms
//每次读取100M,耗时:896ms
//每次读取500M,耗时:769ms
ByteBuffer buf = ByteBuffer.allocateDirect(1024*500);
long d1 = System.currentTimeMillis();
FileInputStream in = null;
FileOutputStream output = null;
FileChannel fic = null;
FileChannel foc = null;
try {
in = new FileInputStream(file);
output = new FileOutputStream(newFile);
fic = in.getChannel();
foc = output.getChannel();
//fic.transferTo(0, fic.size(), foc);
while(fic.read(buf) != -1){
buf.flip();//切换到读取数据模式
foc.write(buf);//将缓冲区的数据写入通道中
buf.clear();//清空缓冲区
}
long d2 = System.currentTimeMillis();
System.out.println("fileChannelDirectRead读取完成,耗时:" + (d2 - d1));
} catch (Exception e) {
e.printStackTrace();
}finally{
try {
if(in != null){
in.close();
}
if(output != null){
output.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 使用IO的缓冲区读取
* @param file
* @param newFile
*/
public static void bufferedRead(File file , File newFile){
//测试:读取1.23GB文件耗时:2202
long d1 = System.currentTimeMillis();
InputStream in = null;
OutputStream output = null;
try {
in = new BufferedInputStream(new FileInputStream(file)) ;
output = new BufferedOutputStream(new FileOutputStream(newFile));
int len = 0;
byte[] data = new byte[1024];
while((len = in.read(data)) != -1){
output.write(data, 0, len);
}
long d2 = System.currentTimeMillis();
System.out.println("bufferedRead读取完成,耗时:" + (d2 - d1));
} catch (Exception e) {
e.printStackTrace();
}finally{
try {
if(in != null){
in.close();
}
if(output != null){
output.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 使用Scanner一行一行读取
* @param file
* @param newFile
*/
public static void scannerRead(File file , File newFile){
//读取1.23GB文件耗时:120945
long d1 = System.currentTimeMillis();
InputStream in = null;
OutputStream output = null;
try {
in = new FileInputStream(file);
output = new FileOutputStream(newFile);
Scanner sc = new Scanner(in, "UTF-8");
//sc.useDelimiter("\\r\\n");
while(sc.hasNext()){
String content = sc.nextLine();
output.write(content.getBytes("UTF-8"));
}
long d2 = System.currentTimeMillis();
System.out.println("scannerRead读取完成,耗时:" + (d2 - d1));
} catch (Exception e) {
e.printStackTrace();
}finally{
try {
if(in != null){
in.close();
}
if(output != null){
output.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public class Server {
public static void main(String[] args) {
try {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannelOne = ServerSocketChannel.open();
serverSocketChannelOne.socket().bind(new InetSocketAddress("127.0.0.1",8080));
serverSocketChannelOne.configureBlocking(false);
//注册channel并指定监听的事件
serverSocketChannelOne.register(selector, SelectionKey.OP_ACCEPT);
ServerSocketChannel serverSocketChannelTwo = ServerSocketChannel.open();
serverSocketChannelTwo.socket().bind(new InetSocketAddress("127.0.0.1",8090));
serverSocketChannelTwo.configureBlocking(false);
//注册channel并指定监听的事件
serverSocketChannelTwo.register(selector, SelectionKey.OP_ACCEPT);
ByteBuffer readBuff = ByteBuffer.allocate(1024);
ByteBuffer writeBuff = ByteBuffer.allocate(1024);
writeBuff.put("received".getBytes());
writeBuff.flip();
while (true) {
int nReady = selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isConnectable()) {
System.out.println(Thread.currentThread().getId()+"start Connectable....");
} else if (selectionKey.isAcceptable()) {
System.out.println(Thread.currentThread().getId()+"start Acceptable....");
ServerSocketChannel serverSocketChannel = (ServerSocketChannel)selectionKey.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}else if (selectionKey.isReadable()) {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
readBuff.clear();
socketChannel.read(readBuff);
readBuff.flip();
System.out.println(Thread.currentThread().getId()+"-received:"+ new String(readBuff.array()));
selectionKey.interestOps(SelectionKey.OP_WRITE);
} else if (selectionKey.isWritable()) {
writeBuff.rewind();
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
socketChannel.write(writeBuff);
selectionKey.interestOps(SelectionKey.OP_READ);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
参考文献
- https://www.cnblogs.com/snailclimb/p/9086334.html
- https://blog.csdn.net/alex_xfboy/article/details/90174840
- https://juejin.im/post/6844903842472001550 (重要:参考多个IO性能对比图)
- https://tech.meituan.com/2016/11/04/nio.html (美团出品,必属精品)
小结
- 编写博客有助于梳理以及思考知识点,也方便回顾,因此定期写博客还是非常有必要的
- 在涉及到IO的场景,小文件(略小于4kb)可优先考虑MMap,否则优先使用FileChannel
《Java程序员修炼之道》:https://github.com/yuanliangding/books/blob/master/计算机-编程语言-JAVA/Java程序员修炼之道.pdf
《Presto技术内幕》