一、基本介绍

  1. Java NIO 全称 Java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 NewIO),是同步非阻塞的。
  2. NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。【基本案例】
  3. NIO 有三大核心部分:Channel(通道,可以理解为BIO中的Socket)、Buffer(缓冲区)、Selector(选择器) 。
  4. NIO 是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。
  5. Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。【后面有案例说明】
  6. 通俗理解:NIO 是可以做到用一个线程来处理多个操作的。假设有 10000 个请求过来,根据实际情况,可以分配 50 或者 100 个线程来处理。不像之前的阻塞 IO 那样,非得分配 10000 个。
  7. HTTP 2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比 HTTP 1.1 大了好几个数量级。
  8. 案例说明 NIO 的 Buffer

    二、NIO 三大核心原理示意图

    一张图描述 NIO 的 Selector、Channel 和 Buffer 的关系。(图片来自某网站)
    image.png

  9. 每个 Channel 都会对应一个 Buffer。

  10. Selector 对应一个线程,一个线程对应多个 Channel(连接)。
  11. 该图反应了有三个 Channel 注册到该 Selector //程序
  12. 程序切换到哪个 Channel 是由事件决定的,Event 就是一个重要的概念。
  13. Selector 会根据不同的事件,在各个通道上切换。
  14. Buffer 就是一个内存块,底层是有一个数组。
  15. 数据的读取写入是通过 Buffer,这个和 BIO是不同的,BIO 中要么是输入流,或者是输出流,不能双向,但是 NIO 的 Buffer 是可以读也可以写,需要 flip 方法切换 Channel 是双向的,可以返回底层操作系统的情况,比如 Linux,底层的操作系统通道就是双向的。

    三、NIO 和 BIO 的比较

  16. BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多。

  17. BIO 是阻塞的,NIO 则是非阻塞的。
  18. BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
  19. Buffer和Channel之间的数据流向是双向的

image.png

四、Buffer

1、buffer 的基本介绍

缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer,如图:【后面举例说明】
image.png

2、buffer 的简单使用

  1. public class BasicBuffer {
  2. public static void main(String[] args) {
  3. //举例说明 Buffer 的使用(简单说明)
  4. //创建一个 Buffer,大小为 5,即可以存放 5 个 int
  5. IntBuffer intBuffer = IntBuffer.allocate(5);
  6. // 向 buffer 中存放数据
  7. for (int i = 0; i < intBuffer.capacity(); i++) {
  8. intBuffer.put(i * 2);
  9. }
  10. // 从 buffer 读取数据
  11. // 将 buffer 转换,读写切换(!!!) 其实就是将 position 置为0,从头开始
  12. intBuffer.flip();
  13. while (intBuffer.hasRemaining()) {
  14. System.out.println(intBuffer.get());
  15. }
  16. }
  17. }

3、Buffer 类及其子类

  1. 在 NIO 中,Buffer 是一个顶层父类,它是一个抽象类,它的实现类中包含了7种基本数据类型(没有 boolean,否则是8种),类的层级关系图:

image.png
以实现类 ShortBuffer 为例,可以看到里面有个 short[] ,这就是存储数据的地方,每个基本数据类型对应的 buffer 实现类,都有一个对应的基本数据类型数组。
image.png

2、Buffer 类定义了所有的缓冲区都具有的四个属性来提供关于其所包含的数据元素的信息:
image.png
为什么 图中的capacity=5,limit=5,因为 IntBuffer intBuffer = IntBuffer.allocate(5);

3、我们基于 buffer 的简单使用,进行debug探索,如下
image.png
每 put 一个数字进入buffer,position 就会+1,直到等于 limit。

4、buffer 的方法一览

buffer 基础方法

image.png

  1. intBuffer.position(1);
  2. intBuffer.limit(3);

ByteBuffer

从前面可以看出对于 Java 中的基本数据类型(boolean 除外),都有一个 Buffer 类型与之相对应,最常用的自然是 ByteBuffer 类(二进制数据),该类的主要方法如下:
image.png

  1. // 会替代0位置上的数字,将其换成1
  2. intBuffer.put(0,1);

关于 NIO ByteBuffer的allocate与allocateDirect区别
参考
ByteBuffer的allocate与allocateDirect区别

五、Channel

1、基本介绍

  1. NIO 的通道类似于流,但有些区别如下:
    • 通道可以同时进行读写,而流只能读或者只能写
    • 通道可以实现异步读写数据
    • 通道可以从缓冲读数据,也可以写数据到缓冲:
  2. BIO 中的 Stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel)是双向的,可以读操作,也可以写操作。
  3. Channel 在 NIO 中是一个接口 public interface Channel extends Closeable{}
  4. 常用的 Channel 类有:FileChannel、DatagramChannel、ServerSocketChannel 和 SocketChannel。【ServerSocketChanne 类似 ServerSocket、SocketChannel 类似 Socket】
  5. FileChannel 用于文件的数据读写,DatagramChannel 用于 UDP 的数据读写,ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写。
  6. 如图

image.png

2、FileChannel 类

FileChannel 主要用来对本地文件进行 IO 操作,常见的方法有

  • public int read(ByteBuffer dst),从通道读取数据并放到缓冲区中
  • public int write(ByteBuffer src),把缓冲区的数据写到通道中
  • public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道
  • public long transferTo(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道

transferTo 非常快,叫什么 零拷贝?

3、应用实例

3.1 本地文件写数据

  1. 使用前面学习后的 ByteBuffer(缓冲)和 FileChannel(通道),将 “hello,supkingx” 写入到 file01.txt 中
  2. 文件不存在就创建
  3. 代码演示

    1. public class NIOFileChannel01 {
    2. public static void main(String[] args) {
    3. String str = "hello,supkingx";
    4. try (FileOutputStream fileOutputStream = new FileOutputStream("file01.txt")) {
    5. // 通过 fileOutputStream 获取对应的 FileChannel
    6. FileChannel fileChannel = fileOutputStream.getChannel();
    7. ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
    8. // 将 Str 放入到 byteBuffer
    9. byteBuffer.put(str.getBytes());
    10. // 对 byteBuffer 进行反转 flip
    11. byteBuffer.flip();
    12. // 将 byteBuffer中的数据写入到 fileChannel
    13. fileChannel.write(byteBuffer);
    14. } catch (Exception e) {
    15. e.printStackTrace();
    16. }
    17. }
    18. }
  4. 写入过程

1-NIO 初识 - 图11

3.2 本地文件读取

  1. 使用前面学习后的 ByteBuffer(缓冲)和 FileChannel(通道),将 file01.txt 中的数据读入到程序,并显示在控制台屏幕
  2. 假定文件已经存在
  3. 代码演示

    1. public class NIOFileChannel02 {
    2. public static void main(String[] args) {
    3. File file = new File("file01.txt");
    4. try(FileInputStream fileInputStream = new FileInputStream(file)) {
    5. FileChannel fileChannel = fileInputStream.getChannel();
    6. // 创建缓冲区
    7. ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
    8. // 将 通道的数据读取到Buffer
    9. fileChannel.read(byteBuffer);
    10. // 将 bytebuffer 的字节数据 转成String
    11. System.out.println(new String(byteBuffer.array()));
    12. } catch (Exception e) {
    13. e.printStackTrace();
    14. }
    15. }
    16. }
  4. 读取过程

1-NIO 初识 - 图12

3.3 使用一个 Buffer 完成数据的读取、写入

  1. public class NIOFileChannel03 {
  2. public static void main(String[] args) {
  3. try (FileInputStream fileInputStream = new FileInputStream("file02.txt");
  4. FileOutputStream fileOutputStream = new FileOutputStream("file022.txt")) {
  5. FileChannel inputChannel = fileInputStream.getChannel();
  6. FileChannel outChannel = fileOutputStream.getChannel();
  7. ByteBuffer byteBuffer = ByteBuffer.allocate(512);
  8. while (true) {
  9. // 这里有一个重要的操作
  10. byteBuffer.clear(); // 清空 buffer
  11. // public final Buffer clear() {
  12. // position = 0;
  13. // limit = capacity;
  14. // mark = -1;
  15. // return this;
  16. // }
  17. int readLen = inputChannel.read(byteBuffer);
  18. System.out.println("readLen=" + readLen);
  19. if (readLen == -1) {
  20. break;
  21. }
  22. // 将 buffer 写入到 outChannel
  23. byteBuffer.flip();
  24. outChannel.write(byteBuffer);
  25. }
  26. } catch (Exception e) {
  27. e.printStackTrace();
  28. }
  29. }
  30. }

3.4 拷贝文件

  1. 实例要求:
  2. 使用 FileChannel(通道)和方法 transferFrom,完成文件的拷贝
  3. 拷贝一张图片
  4. 代码演示

    1. public class NIOFileChannel04 {
    2. public static void main(String[] args) {
    3. try (FileInputStream fileInputStream = new FileInputStream("123.jpg");
    4. FileOutputStream fileOutputStream = new FileOutputStream("king.jpg");
    5. FileChannel inputChannel = fileInputStream.getChannel();
    6. FileChannel outChannel = fileOutputStream.getChannel()) {
    7. // 从 inputChannel 拷贝数据到 outChannel 中去
    8. outChannel.transferFrom(inputChannel, 0, inputChannel.size());
    9. } catch (Exception e) {
    10. e.printStackTrace();
    11. }
    12. }
    13. }

    4、关于 Buffer 和 Channel 的注意事项和细节

  5. ByteBuffer 支持类型化的 put 和 get,put 放入的是什么数据类型,get 就应该使用相应的数据类型来取出,否则可能有 BufferUnderflowException 异常。 ```java ublic class NIOByteBufferPutGet { public static void main(String[] args) {

    1. ByteBuffer byteBuffer = ByteBuffer.allocate(64);
    2. // 类型化放入数据
    3. byteBuffer.putInt(100);
    4. byteBuffer.putLong(9);
    5. byteBuffer.putChar('s');
    6. byteBuffer.putShort((short) 4);
    7. // 取出
    8. byteBuffer.flip();
    9. System.out.println();
    10. System.out.println(byteBuffer.getInt());
    11. System.out.println(byteBuffer.getLong());
    12. System.out.println(byteBuffer.getChar());

    // System.out.println(byteBuffer.getShort());

    1. // 这会报错 BufferUnderflowException
    2. System.out.println(byteBuffer.getLong());

    } }

结果 100 9 s Exception in thread “main” java.nio.BufferUnderflowException at java.nio.Buffer.nextGetIndex(Buffer.java:506) at java.nio.HeapByteBuffer.getLong(HeapByteBuffer.java:415) at com.supkingx.nio.NIOByteBufferPutGet.main(NIOByteBufferPutGet.java:28)

  1. 2. 可以将一个普通 Buffer 转成只读 Buffer
  2. ```java
  3. public class ReadOnlyBuffer {
  4. public static void main(String[] args) {
  5. ByteBuffer byteBuffer = ByteBuffer.allocate(64);
  6. for (int i = 0; i <64; i++) {
  7. byteBuffer.put((byte) i);
  8. }
  9. byteBuffer.flip();
  10. // 得到一个只读 buffer
  11. ByteBuffer readOnlyBuffer = byteBuffer.asReadOnlyBuffer();
  12. System.out.println(readOnlyBuffer.getClass());
  13. // 读取
  14. while (readOnlyBuffer.hasRemaining()){
  15. System.out.println(readOnlyBuffer.get());
  16. }
  17. // 报错 ReadOnlyBufferException
  18. readOnlyBuffer.put((byte) 100);
  19. }
  20. }
  21. 结果
  22. class java.nio.HeapByteBufferR
  23. 0
  24. 1
  25. 2
  26. 3
  27. 4
  28. .....
  29. 63
  30. Exception in thread "main" java.nio.ReadOnlyBufferException
  31. at java.nio.HeapByteBufferR.put(HeapByteBufferR.java:175)
  32. at com.supkingx.nio.ReadOnlyBuffer.main(ReadOnlyBuffer.java:28)
  1. NIO 还提供了 MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改,而如何同步到文件 则由 NIO 来完成。

image.png
file01.txt 中的文字是 hello,supkingx,我们修改其0位置的字符为7,修改其3位置的字符为9,则结果就是 7el9o,supkingx

  1. public class MappedByteBufferTest {
  2. public static void main(String[] args) throws Exception {
  3. RandomAccessFile randomAccessFile = new RandomAccessFile("file01.txt", "rw");
  4. FileChannel channel = randomAccessFile.getChannel();
  5. /**
  6. * 参数 1:FileChannel.MapMode.READ_WRITE 使用的读写模式
  7. * 参数 2:0 可以直接修改的起始位置
  8. * 参数 3:5 是映射到内存的大小(不是索引位置),即将 1.txt 的多少个字节映射到内存
  9. * 可以直接修改的范围就是 0-4 (put(5,xx)就会报错IndexOutOfBoundsException)
  10. * 实际类型 DirectByteBuffer
  11. */
  12. MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
  13. mappedByteBuffer.put(0,(byte) '7');
  14. mappedByteBuffer.put(3,(byte) '9');
  15. channel.close();
  16. randomAccessFile.close();
  17. }
  18. }

通过 debug 可以发现此时的 MappedByteBuffer 是 DirectByteBuffer 类型。
image.png

  1. 前面我们讲的读写操作,都是通过一个 Buffer 完成的,NIO 还支持通过多个 Buffer(即 Buffer数组)完成读写操作,即 Scattering 和 Gathering【举例说明】

    1. ublic class ScatteringAndGatheringTest {
    2. public static void main(String[] args) throws IOException {
    3. // 使用 ServerSocketChannel
    4. ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    5. InetSocketAddress inetSocketAddress = new InetSocketAddress(7070);
    6. // 绑定 端口
    7. serverSocketChannel.socket().bind(inetSocketAddress);
    8. // 创建 buffer 数组
    9. ByteBuffer[] byteBuffers = new ByteBuffer[2];
    10. byteBuffers[0] = ByteBuffer.allocate(5);
    11. byteBuffers[1] = ByteBuffer.allocate(3);
    12. // 等待客户端连接(telnet)
    13. SocketChannel socketChannel = serverSocketChannel.accept();
    14. int messageLength = 8;
    15. while (true) {
    16. int byteRead = 0;
    17. // 数据小于8则进行输出
    18. while (byteRead < messageLength) {
    19. long l = socketChannel.read(byteBuffers);
    20. // 累计读取的字节数
    21. byteRead += l;
    22. System.out.println("byteRead=" + byteRead);
    23. // 输出
    24. Arrays.stream(byteBuffers).map(buffer -> "postion=" + buffer.position() + ",limit=" + buffer.limit()).forEach(System.out::println);
    25. }
    26. // 将所有的 buffer 进行 flip
    27. Arrays.asList(byteBuffers).forEach(Buffer::flip);
    28. // byteBuffers中长度超过messageLength 后将buffer中的数据全部输出到客户端
    29. long byteWrite = 0;
    30. while (byteWrite < messageLength) {
    31. long l = socketChannel.write(byteBuffers);
    32. byteWrite += l;
    33. }
    34. // 将所有的 buffer 进行 clear
    35. Arrays.asList(byteBuffers).forEach(Buffer::clear);
    36. System.out.println("byteRead = " + byteRead + ", byteWrite = " + byteWrite + ", messagelength = " + messageLength);
    37. }
    38. }
    39. }

    六、Selector

    1、基本介绍

  2. Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器)。

  3. Selector 能够检测多个注册的通道(Channel)上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
  4. 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
  5. 避免了多线程之间的上下文切换导致的开销。

    2、Selector 示意图和特点说明

    image.png
    说明如下:

  6. Netty 的 IO 线程 NioEventLoop 聚合了 Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。

  7. 当线程从某客户端 Socket 通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。
  8. 线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。
  9. 由于读写操作都是非阻塞的(数据可以存储在缓存区,然后去干其他事情,可以回到第一部回顾第4小点),这就可以充分提升 IO 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程挂起。
  10. 一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。

    3、Selector 类相关方法

    image.png
    image.png
    image.png
    image.png
    Selector调用selectedKeys方法,返回一个SelectionKey的集合,然后通过key再获取到对应的channel。
    image.png
    select是一个阻塞方法,直到Selector管理的channel集合中,有一个channel产生了事件,这个方法才会返回。
    image.png
    select(_long timeout) 最多阻塞 timeout 时间_
    image.png
    selectNow(),当前没有事件发生,则立刻返回。

    4、注意事项

  11. NIO 中的 ServerSocketChannel 功能类似 ServerSocket、SocketChannel 功能类似 Socket。

  12. Selector 相关方法说明
    • selector.select(); //阻塞
    • selector.select(1000); //阻塞 1000 毫秒,在 1000 毫秒后返回
    • selector.wakeup(); //唤醒 selector
    • selector.selectNow(); //不阻塞,立马返还