JAVANIO

293 阅读9分钟

JAVANIO

总领

随着对高并发的需求,出现了nio,其核心就是缓冲区(Buffer)和通道(Channel),通道就是打开到IO设备(文件,套接字)

JavaNIO (New IO)从1.4引入的新IO API,是面向缓冲区的,基于通道的IO操作,更高效地进行文件读写(非阻塞式)。就是在文件与程序之间架设通道,由缓冲区负责数据的传输并且多了选择器(Selectors),网络编程

相关概念

同步与异步

  • 同步: 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
  • 异步: 异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。

同步和异步的区别最大在于异步的话调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。

阻塞和非阻塞

  • 阻塞: 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
  • 非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。

举个生活中简单的例子,你妈妈让你烧水,小时候你比较笨啊,在那里傻等着水开(同步阻塞)。等你稍微再长大一点,你知道每次烧水的空隙可以去干点其他事,然后只需要时不时来看看水开了没有(同步非阻塞)。后来,你们家用上了水开了会发出声音的壶,这样你就只需要听到响声后就知道水开了,在这期间你可以随便干自己的事情,你需要去倒水了(异步非阻塞)。

2.2 NIO的特性/NIO与IO区别

如果是在面试中回答这个问题,我觉得首先肯定要从 NIO 流是非阻塞 IO 而 IO 流是阻塞 IO 说起。然后,可以从 NIO 的3个核心组件/特性为 NIO 带来的一些改进来分析。如果,你把这些都回答上了我觉得你对于 NIO 就有了更为深入一点的认识,面试官问到你这个问题,你也能很轻松的回答上来了。

1)Non-blocking IO(非阻塞IO)

IO流是阻塞的,NIO流是不阻塞的。

Java NIO使我们可以进行非阻塞IO操作。比如说,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

Java IO的各种流是阻塞的。这意味着,当一个线程调用 read()write() 时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了

2)Buffer(缓冲区)

IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)。

Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中·可以将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而 NIO 却是直接读到 Buffer 中进行操作。

在NIO厍中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。

最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean类型)都对应有一种缓冲区。

3)Channel (通道)

NIO 通过Channel(通道) 进行读写。

通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。

4)Selectors(选择器)

NIO有选择器,而IO没有。

选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。 因此,为了提高系统效率选择器是有用的。

缓冲区存取

总概

缓冲区负责数据存取,底层是数组,用于不同类型数据。所以对于不同数据有不同缓冲区(ByteBuffer,CharBuffer等都是继承于Buffer类),不同缓冲区管理方式基本一致,

1通过allocate()获取缓冲区自定义大小
2缓冲区两个核心方法

put()存入数据到缓冲区(改变position大小)

溢出会抛出java.nio.BufferOverflowException异常

get()获取缓冲区数据(需要调用flip()切换至读模式,position=0,limit改变,随着get调用position会改变)

get()方法如果position=limit情况下使用会抛出java.nio.BufferUnderflowException异常

3缓冲区核心属性(本质数组)

capacity:最大存储容量,不能更改private

limit:表示缓冲区可以操作数据的大小,limit后数据不能进行读写

position:缓冲区当前操作数据的位置

mark用于mark()记录position位置

mark<=position<=limit<=capacity

如果var byteBuffer =ByteBuffer.allocate(1024);则limit=capacity

4flip()切换至读数据模式
  public Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

返回值为当前实例

同时limit改变到已有数据之尾

5rewind()方法可重复读

返回值为当前实例

position变为0

6clear()清空

返回值为当前实例

但是缓冲区的数据还在,处于被遗忘状态,就是指针全部归位,仍存在。

7mark()标记

返回值为当前实例

标记当前position位置,通过reset()恢复到最近一次的mark位置.

8hasRemaining()

返回值位boolean,本质上就是比较position和limit大小

remaining(),返回值位int,本质上就是limit-position,可以操作的数量

直接缓冲区和非直接缓冲区

非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在jvm内存中

直接缓冲区:通过allocateDirect()方法分配缓冲区,将缓冲区建立在物理内存中

上面两种区别在于,非直接缓冲区需要一个copy的过程将内核地址空间的数据copy到用户地址空间,而直接缓冲区是用于物理内存的映射

直接缓冲区的分配与取消分配成本高于非直接缓冲区,直接缓冲区的内容可以驻留在常规垃圾回收堆外,所以容易受到基础系统的本机IO操作的大型的,持久的缓冲区用直接缓冲区,一般小就用非直接缓冲区

直接字节缓冲区还可以通过FileChannel的map()方法将文件区域直接映射到内存中来创建,该方法返回MappedByteBuffer,如果以上缓冲区的实例是不可访问区域,将会在某一时刻抛出不确定异常

字节缓冲区的类型判断可通过isDirect()方法确定,是为了能够在性能关键型代码中执行显式缓冲区管理

通道(Channel)的原理与获取

总概

Channel类似于传统的流,但是其本身不能访问数据,也不存储数据,只能与Buffer进行交互

主要实现类

java.nio.channels.Channel接口

FileChannel,SocketChannel,ServerSocketChannel

获取通道

1getChannel()方法

本地IO

FileInputStream/FileOutputStream

RandomAccessFile

网络IO

​ Socket

​ ServerSocket

​ DatagramSocket

2 jdk1.7NIO2

针对各个通道提供了静态方法open()

FileChannel.open
3jdk1.7NIO2

Files工具类的newByteChannel()

NIO通道数据传输和内存映射文件(直接比非直接快)

非直接缓冲区复制文件(读写)

try(  FileInputStream fileInputStream = new FileInputStream("F:\\psbQ8LFKCE5.jpg");
           FileOutputStream fileOutputStream = new FileOutputStream("E:\\test.jpg");
           FileChannel fileInputStreamChannel = fileInputStream.getChannel();
           FileChannel fileOutputStreamChannel = fileOutputStream.getChannel();)
{   //缓冲区
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
while (fileInputStreamChannel.read(byteBuffer)!=-1)
         {
             byteBuffer.flip();
             fileOutputStreamChannel.write(byteBuffer);
             byteBuffer.clear();

         }
     } catch (IOException e) {
         e.printStackTrace();
     }
    

直接缓冲区复制文件(读写)

程序和复制过程并不是同时结束,程序慢一点,会跑满内存和硬盘读写,和程序无关了只和系统有关,慢一点原因且随机的原因是gc

缓冲区的模式必须和通道模式一致,通道能读能写,缓冲区才能能读能写

 try(FileChannel readChannel = FileChannel.open(Paths.get("F:\\psbQ8LFKCE5.jpg"), StandardOpenOption.READ);
FileChannel writeChannel = FileChannel.open(Path.of("E:\\test.jpg"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE_NEW))
 {
//get多个参数是将多个参数拼串,第二个参数是个枚举
//StandardOpenOption.CREATE_NEW存在就报错,不存在就报错,StandardOpenOption.CREATE存在就覆盖
 //缓冲区在物理内存
//第一个参数为枚举类型
//map返回的就是一个直接缓存区
         MappedByteBuffer readmappedByteBuffer = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, readChannel.size());
         MappedByteBuffer writemappedByteBuffer = writeChannel.map(FileChannel.MapMode.READ_WRITE, 0, readChannel.size());
         //直接对缓冲区进行数据读写
         byte[] bytes = new byte[readmappedByteBuffer.limit()];
         readmappedByteBuffer.get(bytes);
         writemappedByteBuffer.put(bytes);
     } catch (IOException e) {
         e.printStackTrace();
     }
    

通道之间的数据传输(直接缓冲区)

try(  FileChannel readChannel = FileChannel.open(Paths.get("F:\\psbQ8LFKCE5.jpg"), StandardOpenOption.READ);
FileChannel writeChannel = FileChannel.open(Path.of("E:\\test.jpg"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE_NEW);) {
readChannel.transferTo(0, readChannel.size(), writeChannel);
//writeChannel.transferFrom(readChannel,0,readChannel.size());    
     } catch (IOException e) {
         e.printStackTrace();
     }

NIO分散读取和聚合写入

分散读取:将通道内的数据分散到多个缓冲区,依次填满缓冲区

聚合写入:将多个缓冲区数据聚集到通道中

由于汉字编码的奇特性,可能最后一个字会乱码,因为那个字编码不全(utf-8)

try {
         RandomAccessFile randomAccessFile = new RandomAccessFile("F:\\test.txt", "rw");
         FileChannel randomAccessFileChannel = randomAccessFile.getChannel();
         ByteBuffer byteBuffer = ByteBuffer.allocate(100);
         ByteBuffer byteBuffer1 = ByteBuffer.allocate(100);
         ByteBuffer[] byteBuffers={byteBuffer,byteBuffer1};
         //分散读取,参数是一个缓冲区数组
         randomAccessFileChannel.read(byteBuffers);
         Arrays.stream(byteBuffers).map(x->x.flip())
                                   .forEach((ByteBuffer b)-> System.out.println(new String(b.array(),0,b.limit())));
         //聚合写入
         new RandomAccessFile("F:\\2.txt", "rw")
                 .getChannel()
                 .write(byteBuffers);
      randomAccessFile.close();
     } catch (IOException e) {
         e.printStackTrace();
     }

字符集Charset

会有IOException

Charset cs1 = Charset.forName("GBK");
		
		//获取编码器
		CharsetEncoder ce = cs1.newEncoder();
		
		//获取解码器
		CharsetDecoder cd = cs1.newDecoder();
		
		CharBuffer cBuf = CharBuffer.allocate(1024);
		cBuf.put("test");
		cBuf.flip();
		
		//编码
		ByteBuffer bBuf = ce.encode(cBuf);
		
		for (int i = 0; i < 12; i++) {
			System.out.println(bBuf.get());
		}
		
		//解码
		bBuf.flip();
		CharBuffer cBuf2 = cd.decode(bBuf);
		System.out.println(cBuf2.toString());
		
		Charset cs2 = Charset.forName("GBK");
		bBuf.flip();
		CharBuffer cBuf3 = cs2.decode(bBuf);
		System.out.println(cBuf3.toString());

NIO的阻塞与非阻塞