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();
		}
	}
}

参考文献

  1. https://www.cnblogs.com/snailclimb/p/9086334.html
  2. https://blog.csdn.net/alex_xfboy/article/details/90174840
  3. https://juejin.im/post/6844903842472001550 (重要:参考多个IO性能对比图)
  4. https://tech.meituan.com/2016/11/04/nio.html (美团出品,必属精品)

小结

  1. 编写博客有助于梳理以及思考知识点,也方便回顾,因此定期写博客还是非常有必要的
  2. 在涉及到IO的场景,小文件(略小于4kb)可优先考虑MMap,否则优先使用FileChannel
    《Java程序员修炼之道》:https://github.com/yuanliangding/books/blob/master/计算机-编程语言-JAVA/Java程序员修炼之道.pdf
    《Presto技术内幕》
posted @ 2020-10-09 16:48  墨小雨的猫  阅读(182)  评论(0)    收藏  举报