欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

2.3Java NIO

程序员文章站 2024-03-24 14:08:04
...
本文篇幅稍长,但相对易于理解,请耐心食用

基础知识

前情提要:IO和NIO的区别——原有的IO是面向流的、阻塞的,而NIO是面向块的、非阻塞的。

理解面向流的、阻塞的IO(BIO)

对于Java1.4以前的IO模型,一个连接对一个线程

原始的IO是面向流的,没有缓存的概念。

Java IO面向流,意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。而且,它不能前后移动流中的数据。(而NIO可以)

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

总结:简单来说,JavaBIO的线程,1v1男人大战(一个线程对应一个请求)

一一对应(可能会画错,请大佬们指出):

服务器
Accpetor
线程1
线程2
线程3
浏览器1
浏览器2
浏览器3

理解面向块的、非阻塞的NIO

NIO是面向缓冲区的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性。

Java NIO的非阻塞读取,假定一个线程发送请求:“读取数据”,如果目前没有数据可读时,就什么都不会获取,但和BIO不同的是,该线程不会保持阻塞,可以继续做其他的事情,直至数据变的可以读取。(非阻塞写入也是如此)

总结:NIO一个线程对应多个请求,可以做到用一个线程来处理多个操作假设有10000个请求过来,现在就只用分配50或者100个线程来处理。不像之前的阻塞IO那样,规定必须一对一,分配10000个。

背景

BIO带来的挑战——高并发量引起的问题

BIO,即阻塞 I/O,因为不管是磁盘 I/O还是网络 I/O,数据在写入OutputStream 或者 从InputStream读取的时候,都有可能会发生阻塞;一旦阻塞,线程将会失去CPU的使用权,这在当前大规模访问量的性能要求下是会直接GG的。

具体地说,一旦有高并发的大量请求,就会有如下问题:

1、线程根本不够用, 就算使用了线程池复用线程也没啥卵用;

2、BIO模式下,会有大量的线程被阻塞,一直在等待数据,这个时候的线程被挂起,只能干等,CPU利用率很低

3、如果网络I/O堵塞 or 网络抖动 or 网络故障等,线程的阻塞时间可能很长,整个系统会GG;\

进入 NIO 世界

什么是NIO

java.nio全称java non-blocking IO(实际上是 new io),是指JDK 1.4 及以上版本里提供的新api(New IO) ,为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络。

HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个。

Java NIO 概述

Java NIO 由以下几个核心部分组成:

  • Channels
  • Buffers
  • Selectors

虽然 Java NIO 中除此之外还有很多类和组件如 Pipe 和 FileLock,但在Channel,Buffer 和 Selector三者 构成了核心的 API。因此,通篇将集中在这三个组件上。

Channel 和 Buffer

基本上,所有的 IO 在NIO 中都从一个 Channel 开始。数据可以从 Channel 读到 Buffer 中,也可以从 Buffer 写到 Channel 中。

写到Buffer
读到Channel
Channel
Buffer

Selector 和Channel

Selector 允许单线程处理多个 Channel。如果你的程序打开了多个连接(通道Channel),但每个连接的流量都很低,使用 Selector 就会很方便。这是在一个单线程中使用一个 Selector 处理3个 Channel 的示例图:

要使用 Selector,得向 Selector 注册 Channel,然后调用它的 select() 方法。这个方法会一直阻塞,直到某个注册的通道,有事件就绪。一旦这个方法返回了东西,线程就可以处理这些事件。

Thread
Selector
Channel1
Channel2
Channel3

NIO核心实现(详细介绍)

通道Channel

NIO的通道Channel类似于流,但有些区别:

  1. 通道可以同时进行读写,而流只能读 or 只能写

  2. 通道可以异步读写数据

  3. 通道总是要从缓冲Buffer读数据 or 写数据到缓冲Buffer(可以操作缓冲)

这些是 Java NIO 中最重要的通道的实现:

  • FileChannel ( 从文件中读写数据)
  • DatagramChannel(通过 UDP 读写网络中的数据)
  • SocketChannel (通过 TCP 读写网络中的数据)
  • ServerSocketChannel (监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel)

Channel示例

public static void main(String[] args) throws IOException {
    	//我们无法直接打开一个FileChannel,需要通过使用一个InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例
        RandomAccessFile file = new RandomAccessFile("filename", "rw");
        FileChannel fChannel = file.getChannel();
        //分配一个1024比特容量的ByteBuffer
        ByteBuffer buf = ByteBuffer.allocate(1024);

        int bytesRead = fChannel.read(buf);//返回真正读到了多少byte,超过了1024部分的就不读了
        while (bytesRead != -1) {

            System.out.println("Read " + bytesRead);
            buf.flip();//告诉Buffer准备好,要读进来了

            while (buf.hasRemaining()) {
                System.out.print((char) buf.get());//一次一个byte
            }

            buf.clear();
            bytesRead = fChannel.read(buf);//继续读入
        }
        file.close();
    }

了解Channel,那就得了解Buffer,下面会深入讲解Buffer的更多细节。

缓存Buffer

缓冲区本质上是一个可以写入数据的内存块,然后可以再次读取,该对象提供了一组方法,可以更轻松地使用内存块,使用缓冲区读取和写入数据通常遵循以下四个步骤:

  1. 写数据到缓冲区;(抽象了一点)

  2. 调用buffer.flip()方法;

  3. 从缓冲区中读取数据;(抽象了一点)

  4. 调用buffer.clear()或buffer.compat()方法;

Java NIO 有以下Buffer类型

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

这些Buffer类型代表了不同的数据类型。换句话说,就是可以通过char,short,int,long,float 或 double类型来操作缓冲区中的字节

Buffer的索引(基础知识,后面会用到)

buffer的大小/容量 - Capacity

作为一个内存块,Buffer有一个固定的大小值,用参数capacity表示。

当前读/写的位置 - Position

当写数据到缓冲时,position表示当前待写入的位置,position最大可为capacity – 1;当从缓冲读取数据时,position表示从当前位置读取。

信息末尾的位置 - limit

在写模式下,缓冲区的limit表示你最多能往Buffer里写多少数据; 写模式下,limit等于Buffer的capacity,意味着你还能从缓冲区获取多少数据。

下图展示了buffer中三个关键属性capacity,position以及limit在读写模式中的说明:2.3Java NIO

分配Buffer大小

获得一个 Buffer 对象首先要进行分配。 每一个 Buffer 类都有一个 allocate (),如下:

ByteBuffer buf = ByteBuffer.allocate(1024);//单位是byte

向Buffer写数据

写数据到 Buffer 有两种方式:

  • 从 Channel 写到 Buffer。

    int bytesRead = inChannel.read(buf); //read into buffer.
    
  • 通过 Buffer 的 put() 方法写到 Buffer 里。

2.3Java NIO

从Buffer读数据

从Buffer中读取数据有两种方式:

  • 从Buffer读取数据到Channel。

    int bytesWritten = inChannel.write(buf);
    
  • 使用**get()**方法从Buffer中读取数据。(返回类型详见JavaDoc)

2.3Java NIO

flip()

将Buffer从写模式切换到读模式,前面不懂没关系,它的具体操作就是:将position值重置为0,limit的值设置为之前position的值。(看原理就应该了解了吧

clear() vs compact():

  • clear()清空缓冲区,position将被设回0,limit被设置成 capacity的值。但Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据,所以算得上一种“假清空”

  • **compact()将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()**方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。

mark()与reset()方法

通过调用**mark()**方法,可以记录当前position的前一个位置(默认是0)。之后可以通过调用Buffer.reset()方法恢复到这个position。(如果有需要的话)

buffer.mark();//这是是部分代码
buffer.reset();  

选择器Selector

Selector的创建

通过调用**Selector.open()**方法创建一个Selector,如下:

Selector selector = Selector.open();

向Selector注册通道

为了将Channel和Selector配合使用,必须将channel注册到selector上。通过**SelectableChannel.register()**方法来实现,如下:

channel.configureBlocking(false);//非阻塞模式
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);

与Selector一起使用时,Channel必须处于非阻塞模式下。(因为FileChannel不能为非阻塞模式,所以不能将FileChannel与Selector一起使用,但SocketChannel那一类都可以

注意**register()**方法的第二个参数。这是一个“interest集合”(直译:感兴趣的集合,即监听的集合),Selector监听Channel,可以监听四种不同类型的事件:

  • Connect (某个channel成功连接到另一个服务器称为“连接就绪”)

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

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

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

SelectionKey类

Selector维护的三种类型SelectionKey集合:(这一小块下是piao 的,可能阅读有点难受,直接跳了吧)

  • 已注册的键的集合(Registered key set)

    所有与选择器关联的通道所生成的键的集合称为已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过 keys() 方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的;试图这么做的话将引发java.lang.UnsupportedOperationException。

  • 已选择的键的集合(Selected key set)

    所有与选择器关联的通道所生成的键的集合称为已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过 keys() 方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的;试图这么做的话将引发java.lang.UnsupportedOperationException。

  • 已取消的键的集合(Cancelled key set)

    已注册的键的集合的子集,这个集合包含了 cancel() 方法被调用过的键(这个键已经被无效化),但它们还没有被注销。这个集合是选择器对象的私有成员,因而无法直接访问。

在前面中,当向Selector注册Channel时,register()方法会返回一个SelectionKey对象。这个对象包含了一些属性:

  • interest集合
  • ready集合
  • Channel
  • Selector

而前面四种事件可以用SelectionKey的四个常量来表示:

  • SelectionKey.OP_CONNECT

  • SelectionKey.OP_ACCEPT

  • SelectionKey.OP_READ

  • SelectionKey.OP_WRITE

    为什么是常量?你看看这个:

2.3Java NIO

如果你想监听不止一种事件,那么可以用“位或”操作符将常量连接起来,再注册:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey类中属性详解
  • interest集合

    可以通过SelectionKey读写interest集合:

    int interestSet = selectionKey.interestOps();//读到了
    //这样来检查你注册的interest事件
    boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT
    boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
    boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
    boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;
    
  • ready集合

    ready集合是来检测channel中什么事件或操作已经就绪,和interest差不多:

    int readySet = selectionKey.readyOps();
    selectionKey.isAcceptable();//返回boolean类型
    selectionKey.isConnectable();
    selectionKey.isReadable();
    selectionKey.isWritable();
    
  • 从SelectionKey访问Channel和Selector

    Channel  channel  = selectionKey.channel();
    Selector selector = selectionKey.selector();
    

从Selector中选择Channel

select()方法
  • int select():阻塞到至少有一个通道在你注册的事件上就绪了。
  • int select(long timeout):和select()一样,但最长阻塞时间为timeout毫秒。
  • int selectNow():非阻塞,只要有通道就绪就立刻返回。

select()方法返回的int值,是自上次调用select()方法后,有多少通道变成就绪状态的数目。也就是说,之前在select()调用时进入就绪的通道不会在本次调用中被记入。

另外再说明一下,一旦在Selector注册了Channel,就可以用Selector.select()方法返回你既感兴趣的事件,又准备就绪的那些Channel个数(默认为0)。(也就是当前既在interest集合,又在ready集合里面的事件)

selectedKeys()方法

一旦Selector.select()返回值1\geq1,就可以用Selector.selectedKeys()方法,访问“已选择键集(selected key set)”中的就绪Channel。(也就是前面所讲到的第二种SelectionKey)

//这段代码是piao的
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.
    } else if (key.isConnectable()) {
        // a connection was established with a remote server.
    } else if (key.isReadable()) {
        // a channel is ready for reading
    } else if (key.isWritable()) {
        // a channel is ready for writing
    }
    keyIterator.remove();
}
其他问题

Selector执行选择的过程中,系统底层会依次遍历每个Channel是否已经就绪,这个过程可能会造成调用线程进入阻塞状态,那么我们有以下三种方式可以唤醒在select()方法中阻塞的线程。

wakeUp()方法

通过调用Selector对象的wakeup()方法,让处在阻塞状态的select()方法立刻return

close()方法

用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。但是Channel本身并不会关闭

大汇总

最后一波大汇总体验一下NIO的工作方式吧:

public class Test {
    public void selector() throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        Selector selector = Selector.open();
        ServerSocketChannel ssc = ServerSocketChannel.open();//这里要open,前面没有讲Channel具体的子类,疏忽了
        ssc.configureBlocking(false);//非阻塞方式
        ssc.socket().bind(new InetSocketAddress(8080));//端口号
        ssc.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            Set selectedKeys = selector.selectedKeys();//取得集合
            Iterator it = selectedKeys.iterator();//弄到迭代器里面操作方便
            while (it.hasNext()) {
                SelectionKey key = (SelectionKey) it.next();
                //只展示accept和read
                if (key.isAcceptable()) {
                    ServerSocketChannel ssc01 = (ServerSocketChannel) key.channel();
                    SocketChannel sc = ssc01.accept();//接收到服务端的请求,详见2.3里面的网络I/O工作机制
                    sc.configureBlocking(false);
                    sc.register(selector, SelectionKey.OP_READ);
                    it.remove();
                } else if (key.isReadable()) {
                    SocketChannel sc = (SocketChannel) key.channel();
                    while (true) {
                        buffer.clear();
                        int n = sc.read(buffer);//这里读数据啦
                        if (n <= 0) {
                            break;//没得读的就退了
                        }
                        buffer.flip();
                    }
                    it.remove();
                }
            }
        }

    }
}


相关标签: 深入理解JavaWeb