Java NIO核心概念总结篇

一、Java NIO基本介绍

Java NIO(New IO,也有人叫:Non Blocking IO)是从Java1.4版本开始引入的一个新的IO API,其与原来的IO有同样的作用和目的,但是使用方式有很大的差别。NIO是为提供I/O吞吐量而专门设计,其卓越的性能甚至可以与C媲美。NIO是通过Reactor模式的事件驱动机制来达到Non blocking的,那么什么是Reactor模式呢?Reactor翻译成中文是“反应器”,就是我们将事件注册到Reactor中,当有相应的事件发生时,Reactor便会告知我们有哪些事件发生了,我们再根据具体的事件去做相应的处理。

NIO支持面向缓冲区的、基于通道的IO操作,将以更加高效的方式进行文件的读写操作。

NIO的三个核心模块:Buffer(缓冲区)、Channel(通道)、Selector(选择器)。

二、Java NIO与IO的主要区别

IO NIO
面向流(Stream Oriented) 面向缓冲区(Buffer Oriented)
阻塞IO(Blocking IO) 非阻塞IO(Non Blocking IO)
无 选择器(Selectors)

三、通道和缓冲区

1.缓冲区:

1.1 基本概念:

缓冲区(Buffer)就是在内存中预留指定字节数的存储空间用来对输入/输出(I/O)的数据作临时存储,这部分预留的内存空间就叫做缓冲区;

1.2 作用:

用来临时存储数据,可以理解为是I/O操作中数据的中转站。缓冲区直接为通道(Channel)服务,写入数据到通道或从通道读取数据,这样的操利用缓冲区数据来传递就可以达到对数据高效处理的目的。

1.3 类型:

Buffer就像一个数组,可以保存多个相同类型的数据,根据数据类型的不同(Boolean类型除外),有以下七个Buffer常用的子类:ByteBuffer、CharBuffer 、ShortBuffer 、IntBuffer 、LongBuffer 、FloatBuffer 、DoubleBuffer 。

1.4 缓冲区的四个基本属性

属性 概念
容量(capacity) 表示Buffer最大的数据容量,缓冲区的容量不能为负数,而且一旦创建,不可修改
限制(limit) 缓冲区中当前的数据量,即位于limit之后的数据不可读写
位置(position) 下一个要读取或写入的数据的索引
标记(mark) 调用mark()方法来记录一个特定的位置:mark=position,然后再调用reset()可以让position恢复到标记的位置即position=mark

容量、限制、位置、标记遵守以下不变式:0 <= mark <= position <= limit <= capacity

1.5 创建缓冲区:

获取一个指定容量的xxxBuffer对象,以ByteBuffer为例:

创建一个容量大小为1024的ByteBuffer数组,需要注意的是:所有的缓冲区类都不能直接使用new关键字实例化,它们都是抽象类,但是它们都有一个用于创建相应实例的静态工厂方法:static XxxBuffer allocate(int capacity);

1.6 缓冲区的两个数据操作方法:

put()和get()

get();获取Buffer中的数据put(),放入数据到Buffer中。

1.7 flip()方法:

将写数据状态切换为读数据状态

flip()的源码:

public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
}

具体操作代码如下:

// 1.创建一个容量为1024的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
        
// 2.往缓冲区里写数据
String str = "abcde";
buf.put(str.getBytes());
        
// 3.切换数据模式
buf.flip();
        
// 4.读取数据
byte[] dst = new byte[buf.limit()];
buf.get(dst);

1.8 直接缓冲区与非直接缓冲区

(1)非直接缓冲区

首先看看非直接缓冲区。我们之前说过NIO通过通道连接磁盘文件与应用程序,通过缓冲区存取数据进行双向的数据传输。物理磁盘的存取是操作系统进行管理的,与物理磁盘中的数据操作需要经过内核地址空间;而我们的Java应用程序是通过JVM分配的缓冲空间。有点雷同于一个属于核心态,一个属于应用态的意思,而数据需要在内核地址空间和用户地址空间,在操作系统和JVM之间进行数据的来回拷贝,无形中增加的中间环节使得效率与后面要提的之间缓冲区相比偏低。

(2)直接缓冲区

直接缓冲区则不再通过内核地址空间和用户地址空间的缓存数据的复制传递,而是在物理内存中申请了一块空间,这块空间映射到内核地址空间和用户地址空间,应用程序与磁盘之间的数据存取之间通过这块直接申请的物理内存进行。

(3)直接与非直接缓冲区的要点

字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用操作系统基础的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。

直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在 直接缓冲区能在程序性能方面带来明显好处时 分配它们。

直接字节缓冲区还可以通过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer 。 Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。

字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定。提供此方法是为了能够在性能关键型代码中执行显式缓冲区管理

那么既然直接缓冲区的性能更高、效率更快,为什么还要存在两种缓冲区呢?因为直接缓冲区也存在着一些缺点:

1)不安全

2)消耗更多,因为它不是在JVM中直接开辟空间。这部分内存的回收只能依赖于垃圾回收机制,垃圾什么时候回收不受我们控制。

3)数据写入物理内存缓冲区中,程序就丧失了对这些数据的管理,即什么时候这些数据被最终写入从磁盘只能由操作系统来决定,应用程序无法再干涉。

(4)选择方法

直接缓冲区适合与数据长时间存在于内存,或者大数据量的操作时更加适合。

2.通道:

2.1 基本概念:

表示IO源与目标(例如:文件、套接字)打开的连接。但是通道(Channel)本身不能直接访问数据,需要与缓冲区(Buffer)配合才能实现数据的读取操作。

2.2 作用:

如果把缓冲区理解为火车,那么通道就是铁路,即通道(Channel)负责传输,缓存区(Buffer)负责存储。

2.3 Channel的主要实现类:

实现类 概念
FileChannel 用于本地读取、写入、映射和操作文件的通道
DatagramChannel 通过UDP读写网络中的数据通道
SocketChannel 通过TCP读写网络中的数据通道
ServerSocketChannel 可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel

2.4 支持通道的类:

用于本地IO操作:FileInputStream、FileOutputStream、RandomAccessFile(随机文件存储流)

用于网络IO操作:DatagramSocket、Socket、ServerSocket

2.5 获取通道的三种方法

方式一:getChannel()方法

FileInputStream fis = new FileInputStream("NIO.pdf");
FileOutputStream fos = new FileOutputStream("newNIO.pdf");
// 获取通道 
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel();

方式二:通过Files类的静态方法newByteChannel()获取

FileChannel inChannel = null;
FileChannel outChannel = null;      
// 获取通道 
inChannel = (FileChannel) Files.newByteChannel(Paths.get("NIO.pdf"), StandardOpenOption.READ);
outChannel = (FileChannel) Files.newByteChannel(Paths.get("newNIO.pdf"), 
           StandardOpenOption.READ, StandardOpenOption.WRITE,StandardOpenOption.CREATE);

方式三:通过通道的静态方法:open()获取

FileChannel inChannel = null;
FileChannel outChannel = null;
// 获取通道
inChannel = FileChannel.open(Paths.get("NIO.pdf"), StandardOpenOption.READ);
outChannel = FileChannel.open(Paths.get("newNIO.pdf"), StandardOpenOption.READ,     
                                  StandardOpenOption.WRITE, StandardOpenOption.CREATE);

2.6 通道中的数据传输

(1) 将Buffer中的数据写入Channel中:

int bytesWritten = inChannel.write(buf);
(2)从Channel读取数据到Buffer中:

int bytesRead = inChannel.read(buf);

四、分散和聚集

分散读取(Scatter)指从Channel中读取的数据“分散”到多个Buffer中。按照缓冲区的顺序,从Channel中读取的数据依次将Buffer填满。

聚集写入(Gather)指将多个Buffer中的数据“聚集”到Channel中。按照缓冲区的顺序,写入position和limit之间的数据到Channel中去。

transferFrom和transferTo用法:

transferFrom用法:

RandomAccessFile fromFile = new RandomAccessFile("fromNIO.pdf", "rw");
 
// 获取FileChannel
FileChannel fromChannel = fromFile.getChannel();
 
RandomAccessFile toFile = new RandomAccessFile("toNIO.pdf", "rw");
FileChannel toChannel = toFile.getChannel();
 
// 定义传输位置
long position = oL;
 
// 最多传输的字节数
long count = fromChannel.size();
 
// 将数据从源通道传输到另一个通道
toChannel.transferFrom(fromChannel, count, position);
transferTo()用法:和transferFrom相比,position和fromChannel这两个参数的位置换了

RandomAccessFile fromFile = new RandomAccessFile("fromNIO.pdf", "rw");
 
// 获取FileChannel
FileChannel fromChannel = fromFile.getChannel();
 
RandomAccessFile toFile = new RandomAccessFile("toNIO.pdf", "rw");
FileChannel toChannel = toFile.getChannel();
 
// 定义传输位置
long position = oL;
 
// 最多传输的字节数
long count = fromChannel.size();
 
// 将数据从源通道传输到另一个通道
toChannel.transferFrom(position, count, fromChannel);

五、阻塞与非阻塞

阻塞:

基本概念:传统的IO流都是阻塞式的。也就是说,当一个线程调用read()或者write()方法时,该线程将被阻塞,直到有一些数据读读取或者被写入,在此期间,该线程不能执行其他任何任务。在完成网络通信进行IO操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量的客户端时,性能急剧下降。

非阻塞:

基本概念:Java NIO是非阻塞式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程会去执行其他任务。线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。因此NIO可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。

下面给出一个例子:分别用阻塞和非阻塞的方式实现,内容中涉及到的选择器(Selector)会在后面进行讲解。

阻塞式:

// NIO的阻塞
public class TestNIOBlockDemo1 {
    
    // 客户端
    @Test
    public void client() throws IOException{
        
        SocketChannel socketChannel = null;
        FileChannel inChannel = null;
        
        // 1 创建一个socket连接
        socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
        
        // 2 获取通道
        inChannel = FileChannel.open(Paths.get("file/1.txt"), StandardOpenOption.READ);
        
        // 3 分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        
        // 4 读取本地文件,并发送到服务端
        while(inChannel.read(buf) != -1){    // 不等于-1,就说明读到了东西
            buf.flip();  // 将读模式转换为写模式
            socketChannel.write(buf);
            buf.clear(); // 清空缓存区
        }
        
        // 5 通知服务端,客户端已经传输完毕
        socketChannel.shutdownOutput();
        
        // 6 接收服务端的反馈信息
        int len = 0;
        while((len = socketChannel.read(buf)) != -1){
            buf.flip();
            System.out.println(new String(buf.array(), 0, len));
            buf.clear();
        }
        
        //7 关闭通道
        socketChannel.close();
        inChannel.close();
    }
    
    // 服务端
    @Test
    public void server() throws IOException{
        // 1 获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        
        FileChannel outChannel = FileChannel.open(Paths.get("file/2.txt"), StandardOpenOption.READ, StandardOpenOption.CREATE);
        
        // 2 绑定连接
        serverSocketChannel.bind(new InetSocketAddress(9898));
        
        // 3 获取客户端的连接通道
        SocketChannel socketChannel = serverSocketChannel.accept();
        
        // 4 分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        
        // 5 接收客户端的数据,并保存到本地
        while(socketChannel.read(buf) != -1){
            buf.flip();
            outChannel.write(buf);
            buf.clear();
        }
        
        // 6 接收完成,发送反馈给客户端
        buf.put("服务端接收数据成功!".getBytes());
        buf.flip();
        socketChannel.write(buf);
        
        // 7 关闭通道
        socketChannel.close();
        outChannel.close();
        serverSocketChannel.close();
    }
    
}

非阻塞式:

// 非阻塞NIO
public class TestNonNIOBlockDemo1 {
    
    // 客户端
    @Test
    public void client() throws IOException{
        // 1.获取通道
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
        
        // 2.切换非阻塞模式
        socketChannel.configureBlocking(false);
        
        // 3.分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        
        // 4. 发送数据给服务端
        Scanner scan = new Scanner(System.in);
        while(scan.hasNext()){
            String str = scan.next();
            buf.put((new Date().toString() + "\n" + str).getBytes());
            buf.flip();
            socketChannel.write(buf);
            buf.clear();
        }
        
        // 5.关闭通道
        socketChannel.close();
    }
    
    
    // 服务端
    @Test
    public void server() throws IOException{
        // 1.获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        
        // 2.切换非阻塞式模式
        serverSocketChannel.configureBlocking(false);
        
        // 3.绑定连接
        serverSocketChannel.bind(new InetSocketAddress(9898));
        
        // 4.获取选择器
        Selector selector = Selector.open();
        
        // 5.将通道注册到选择器上,并指定“监听接收事件”
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        
        // 6.轮询式的获取选择器上已经“准备就绪”的事件
        while(selector.select() > 0){
            
            // 7.获取当前选择器中所有注册的“选择键(已经就绪的监听事件)”
            Iterator it = selector.selectedKeys().iterator();
            
            while(it.hasNext()){
                // 8.获取准备“就绪”的事件
                SelectionKey sk = it.next();
                
                // 9.判断具体是什么事件准备就绪,不同的事件有不同的处理方法
                if(sk.isAcceptable()){
                    // 10.若是“接收就绪”,获取客户端连接状态通道
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    
                    // 11.切换非阻塞式模式
                    socketChannel.configureBlocking(false);
                    
                    // 12.将该通道注册到选择器上
                    socketChannel.register(selector, SelectionKey.OP_READ);
                }
                else if(sk.isReadable()){
                    // 13.获取当前选择器上“读就绪”状态的通道
                    SocketChannel socketChannel = (SocketChannel) sk.channel();
                    
                    // 14.读取数据
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    
                    int len = 0;
                    while((len = socketChannel.read(buf)) > 0){
                        buf.flip();
                        System.out.println(new String(buf.array(), 0, len));
                        buf.clear();
                    }
                }
                
                // 15.取消选择键SelectionKey
                it.remove();
            }
        }
    }
    
}

ps:从上面的例子也可以看出来使用NIO完成网络通信的三个核心为:

1、通道(Channel):负责连接

2、缓冲区(Buffer):负责数据的存储

3、选择器(Selector):监控状态

六、选择器(Selector)

1、基本概念

选择器(Selector):是可选择通道(SelectableChannel)对象的多路复用器,Selector可以同时监控多个SelectableChannel的IO状况,提供了询问通道是否已经准备好执行每个I/O操作的能力,即利用Selector可以使一个单独的线程管理多个Channel,这样会大量的减少线程之间上下文切换的开销。

可选择通道(SelectableChannel):SelectableChannel这个抽象类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。因为FileChannel类没有继承SelectableChannel因此是不是可选通道,而所有socket通道都是可选择的,包括从管道(Pipe)对象的中获得的通道。SelectableChannel可以被注册到Selector对象上,同时可以指定对那个选择器而言,那种操作是感兴趣的。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。

选择器键(SelectionKey):表示SelectableChannel和Selector之间的注册关系。每次像选择器注册通道时就会选择一个事件(选择键)。选择键包含两个数值的操作集,指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。

使用Selector管理多个channel的结构图

2、Selector的使用

1、创建选择器(Selector)

Selector selector = Selector.open();
 2、将Channel注册到Selector

// Channel必须处于非阻塞模式下
channel.configureBlocking(false);
// 将Channel注册到Selector中
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

register参数说明:

第一个参数:selector代表注册到哪个选择器中;

第二个参数:是“interest集合”,表示选择器所关心的通道操作,它实际上是一个表示选择器在检查通道就绪状态时需要关心的操作的比特掩码。比如一个选择器对通道的read和write操作感兴趣,那么选择器在检查该通道时,只会检查通道的read和write操作是否已经处在就绪状态。它有以下四种可以监听的事件类型:

事件类型 表示方法
读 SelectionKey.OP_READ
写 SelectionKey.OP_WRITE
连接 SelectionKey.OP_CONNECT
接收 SelectionKey.OP_ACCEPT
若注册时不止监听一个事件,则可以使用“位或”操作符连接:

int interestSet = SelectionKey.OP_READ|SelectionKey.OP_WRITE
当通道触发了某个操作之后,表示该通道的某个操作已经就绪,可以被操作。因此:

某个SocketChannel成功连接到另一个服务器称为“连接就绪”(OP_CONNECT);

一个ServerSocketChannel准备好接收新进入的连接称为“接收就绪”(OP_ACCEPT);

一个有数据可读的通道可以说是“读就绪”(OP_READ);

等待写数据的通道可以说是“写就绪”(OP_WRITE)。

3、取消选择键SelectionKey
remove();方法

七、管道(Pipe)

管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会从source通道读取,并且写入到sink通道。

public class PipeTest {
    
    @Test
    public void test() throws IOException{
        String str = "pcwl_java";
        // 创建管道
        Pipe pipe = Pipe.open();
        
        // 向管道中写入数据,需要访问sink通道 ,SinkChannel是内部类
        Pipe.SinkChannel sinkChannel = pipe.sink();
        
        // 通过SinkChannel的write()方法写数据
        ByteBuffer buf1 = ByteBuffer.allocate(1024);
        buf1.clear();
        buf1.put(str.getBytes());
        buf1.flip();
        while(buf1.hasRemaining()){
            sinkChannel.write(buf1);
        }
        
        // 从管道中读取数据,需要访问source通道,SourceChannel是内部类
        Pipe.SourceChannel sourceChannel = pipe.source();
        
        // 调用source通道的read()方法来读取数据
        ByteBuffer buf2 = ByteBuffer.allocate(1024);
        sourceChannel.read(buf2);
    }
}

你可能感兴趣的:(Java NIO核心概念总结篇)