NIO与BIO的区别

NIO是jdk1.4引入的新的IO api,可以替代原来的标准java io(以下简称BIO,阻塞IO),NIO是面向缓冲区,基于通道的非阻塞io操作,BIO是面向流的阻塞性io操作。nio可以做到更加高效的文件读写操作。

BIO

NIO

面向流

面向缓冲区

阻塞

非阻塞


选择器

NIO三大核心概述

1、缓冲区

缓冲区(buffer):主要是用来存储数据。所有缓冲区都是Buffer抽象类的子类。

NIO中的缓冲区主要是用于跟通道进行交互完成数据传输。数据总是从通道读取到缓冲区,然后从缓冲区写入通道中。

2、通道

通道表示打开到 IO 设备(例如:文件、 套接字)的连接。通道必须依赖缓冲区才能传输数据。

3、选择器

非阻塞IO的核心,通过将选择器注册到通道,选择器可以监听通道的事件(读,写,连接,接收),当选择器监听到某个通道准备好时,才会去执行IO操作,能够让一个单独的线程管理多个通道,所以选择器也被称为多路复用器。

缓冲区详解

根据数据类型不同(boolean 除外),主要有以下缓冲区:
    -|  ByteBuffer
    -|  CharBuffer
    -|  ShortBuffer
    -|  IntBuffer
    -|  LongBuffer
    -|  FloatBuffer
    -|  DoubleBuffer

1、缓冲区基本属性

position:下一个可读写操作位置

limit:最大读写限制

capacity:buffer总容量

mark()和reset():标记当前位置,通过reset将position重置到mark位置

结论: 0 <= mark <= position <= limit <= capacity

2、常用方法和数据存取操作:

@Test
public void test1() {
    // 1.分配指定大小的缓冲区
    ByteBuffer buffer = ByteBuffer.allocate(1024);

    System.out.println(buffer.position()); // 0,初始化时,position为零
    System.out.println(buffer.limit()); // 1024,初始化时,limit跟容量保持一致
    System.out.println(buffer.capacity()); // 1024,初始化的容量

    // 2.通过put()往缓冲区存放数据
    String str = "abc";
    buffer.put(str.getBytes()); // 往缓冲区存放长度为3的字符串

    System.out.println(buffer.position()); // 3,position保持跟缓冲区数据长度增长一致
    System.out.println(buffer.limit()); // 1024,最大存放限制跟容量一致
    System.out.println(buffer.capacity()); // 1024

    // 3.切换到读取模式
    buffer.flip();

    System.out.println(buffer.position()); // 0,将position重置到开始的位置
    System.out.println(buffer.limit()); // 3, 切换到读取模式时,将limit设定成缓冲区数据的长度
    System.out.println(buffer.capacity()); // 1024

    // 4.读取缓冲区数据到byte数组中
    byte[] dest = new byte[buffer.limit()]; // 切换到读取模式时,limit切换成缓冲区已经插入数据的长度
    buffer.get(dest);
    System.out.println(new String(dest, 0, dest.length));

    System.out.println(buffer.position()); // 3,读取完将position保持数据长度一致
    System.out.println(buffer.limit()); // 3,读取完将limit保持数据长度一致
    System.out.println(buffer.capacity()); // 1024

    // 5.将缓冲区设置为可重复读
    buffer.rewind();

    System.out.println(buffer.position()); // 0,重置
    System.out.println(buffer.limit()); // 3,设定为已插入数据的长度
    System.out.println(buffer.capacity()); // 1024

    // 6.清空缓冲区
    buffer.clear();

    System.out.println(buffer.position()); // 0,重置到初始化状态
    System.out.println(buffer.limit()); // 1024,重置到初始化状态
    System.out.println(buffer.capacity()); // 1024,重置到初始化状态
    System.out.println((char) buffer.get(0)); // a: 清空只是将重置缓冲区的属性到初始状态,并未真正删除数据
}

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

非直接缓冲区主要是在JVM堆内存,直接缓冲区是直接建立在物理内存中,能够提升数据传输效率。

1)通过ByteBuffer.allocateDirect(int)直接分配到物理内存。

2)FileChannel的map()方法返回MappedByteBuffer,

3)改缓冲区直接分配在内存中。可以通过isDirect()方法判断缓冲区是否分配到内存中。

通道详解

1、获取通道的方式,直接代码演示
   创建通道的方式
   1)通过支持通道的对象
       -|  FileInputStream
       -|  FileOutputStream
       -|  RandomAccessFile
       -|  Socket
       -|  ServerSocket
       -|  DatagramSocket
   2)通道类的open方法
       -|  FileChannel
       -|  SocketChannel
       -|  ServerSocketChannel
       -|  DatagramChannel
   3)通过Files的静态方法newByteChannel()获取字节通道
      -|  Files
 

@Test
public void test3() throws IOException {
    // 1、通过支持通道的对象,调用getChannel()获取
    FileInputStream fis = new FileInputStream("1.txt");
    FileChannel channel1 = fis.getChannel();
    FileOutputStream fos = new FileOutputStream("2.txt");
    FileChannel channel2 = fos.getChannel();
    RandomAccessFile randomAccessFile = new RandomAccessFile("a.txt", "rw");
    FileChannel channel3 = randomAccessFile.getChannel();
    Socket socket = new Socket();
    SocketChannel channel4 = socket.getChannel();
    ServerSocket serverSocket = new ServerSocket();
    ServerSocketChannel channel5 = serverSocket.getChannel();
    DatagramSocket datagramSocket = new DatagramSocket();
    DatagramChannel channel6 = datagramSocket.getChannel();
    
    // 2、通道类的open方法
    FileChannel channel7 = FileChannel.open(Paths.get("1.txt"), StandardOpenOption.READ);
    FileChannel channel8 = FileChannel.open(Paths.get("1.txt"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
    SocketChannel channel9 = SocketChannel.open();
    ServerSocketChannel channel10 = ServerSocketChannel.open();
    DatagramChannel channel11 = DatagramChannel.open();
    
    // 3、通过Files的静态方法newByteChannel()获取
    SeekableByteChannel channel12 = Files.newByteChannel(Paths.get("1.txt"), StandardOpenOption.WRITE);
}

2、 通道配合缓冲区完成文件复制

@Test
public void test4() {

    FileChannel inChannel = null;
    FileChannel outChannel = null;

    try {
        // 1、创建直接缓冲区(在堆外,物理内存)
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
        // 2、创建通道
        inChannel = FileChannel.open(Paths.get("D:/file.txt"), StandardOpenOption.READ);
        // 可读写,没有该文件就创建,有则报错,也已直接用StandardOpenOption.NEW没有就不创建,不会报错
        outChannel = FileChannel.open(Paths.get("D:/dest.txt"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
        while (inChannel.read(buffer) != -1) {
            buffer.flip(); // 切换缓冲区为读取模式
            outChannel.write(buffer); // 将缓冲区数据写入通道
            buffer.clear(); // 清空缓冲区
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (inChannel != null) {
            try {
                inChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (outChannel != null) {
            try {
                outChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

选择器详解

阻塞:传统的IO流都是阻塞的,当一个线程调用read()或write()方法读取或写入数据时,该线程会进入阻塞状态,每个读写请求都要分配一个线程去处理,当处理大量客户请求时,服务器性能就会急剧下降。

非阻塞:nio是非阻塞模式,当线程从某通道读写数据时,若没有数据可用时,改线程可以去处理其他通道的io操作,所以服务可以使用一个或少量的线程来管理服务器的所有客户端。NIO的非阻塞io主要是选择器来实现的。而且主要用于网络io。

选择器:多路复用器,selector可以监控多个通道的io状况,能够让一个单独线程管理多个通道。selector是非阻塞io的核心。

SelectableChannel是可以使用选择器的通道抽象父类,主要子类是:

    -|  SocketChannel

    -|  ServerSocketChannel

    -|  DatagramChannel

在选择器注册到通道时候可以配置具体的监听事件,主要有:

读 : SelectionKey.OP_READ

写 : SelectionKey.OP_WRITE

连接 : SelectionKey.OP_CONNECT

接收 : SelectionKey.OP_ACCEPT

只有选择器上具体的监听事件就绪时,线程才会去处理该通道上操作,而不会一直阻塞等待。