NIO模型

NIO:同步非阻塞模型

采用事件驱动的思想来实现一个多路复用器,主要是来解决高并发问题

NIO中存在IO复用器,一个复用器可以同时监听多个用户的连接或者读写操作,基于事件驱动,一个复用器通过一个线程来管理,意味着一个线程可以处理多个用户的请求事件

NIO底层本质采用的是IO复用模型

NIO中提供了(selector)选择器也叫做多路复用器,作用是检查一个或者多个NIO channel的状态是否可读、可写等。可以实现单线程管理多个channel,也可以管理多个网络请求。

channel(通道):用于IO操作的连接,是对原有IO 的一种补充,不能直接访问数据,需要和缓冲区buffer进行交互

ServerSocketChannel:监听新进来TCP的连接,对于每一个连接都需要创建一个SocketChannel,一般在服务端实现

SocketChannel:通过TCP读写网络中的数据,一般在客户端实现

Buffer(缓冲区):IO 流中数据需要经过缓冲区才能交给channel

NIO处理用户请求:

 思路:和Socket编程流程类似(BIO),当前是非阻塞的处理,将原来BIO中阻塞的业务改成非阻塞,即把原BIO阻塞的位置(accept、connect、read、write)在NIO中设置为非阻塞+复用器

服务端编程:

/**
 * NIO服务端编程
 */
public class Server {
    public static void main(String[] args) {
        startServer(7779);
    }

    public static void startServer(int port) {
        try {
            //创建ServerSocketChannel通道
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

            //绑定端口
            serverSocketChannel.bind(new InetSocketAddress(port));
            System.out.println("服务端绑定端口:"+port+"并启动啦");

            /**
             * 进行accept接收客户端连接
             * 该方法在BIO中是阻塞的,
             * 在NIO中需要设置为非阻塞,将其交给复用器管理监听事件
             */
            //将通道设置为非阻塞
            serverSocketChannel.configureBlocking(false);

            //创建selector复用器实例
            Selector selector = Selector.open();

            //将通道注册到复用器上并关注可接收事件
            serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);

            //等待复用器监听结果,通过调用select方法阻塞等待,直到有事件发生
            //循环监听复用器是否有事件准备就绪
            while (selector.select()> 0) {
                //selectedKeys 感兴趣事件的集合
                Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();
                    //判断是否是可接收事件
                    if (selectionKey.isAcceptable()) {
                        //当前有可接收事件发生,即由客户端连接
                        System.out.println("有可接收事件发生");
                        ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();

                        //通过accept来接收客户端的连接,accept不是阻塞的了
                        SocketChannel socketChannel = serverSocketChannel1.accept();

                        //可以进行读写操作,需要将通道设置为非阻塞
                        socketChannel.configureBlocking(false);

                        //将socketChannel通道注册到复用器上,并关注读事件
                        socketChannel.register(selector,SelectionKey.OP_READ);
                    }

                    //判断是否是可读事件
                    if (selectionKey.isReadable()) {
                        System.out.println("有可读事件发生");
                        //获取通道,通过通道来读取数据
                        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();

                        //创建buffer
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        //将通道数据写入buff
                        socketChannel.read(byteBuffer);
                        //对buff进行读写模式切换
                        byteBuffer.flip();
                        //将数据从buff中读取出来
                        byte[] bytes = new byte[byteBuffer.remaining()];
                        byteBuffer.get(bytes);
                        String msg = new String(bytes);
                        System.out.println("客户端发送数据:"+msg);
                    }
                }
            }

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

NIO服务端编程流程:

  1. 创建serverSockerChannel通道实例
  2. serverSockerChannel通道绑定端口
  3. serverSockerChannel通道绑定端口设置为非阻塞serverSocketChannel.configureBlocking(false);
  4. 创建selector复用器实例
  5. 将serverSocketChannel通道注册到selector复用器上,监听可接收事件
  6. 由复用器来监听事件是否发生
  7. 有事件发生,select会返回,需要来遍历感兴趣事件集合selectionKeys
  8. 如果是可接收事件,获取通道,通过serverSockerChannel通道实例来接收accept客户端的连接的实例socketChannel
  9. 将socketChannel实例设置为非阻塞,将通道注册到复用器上,并监听可读事件
  10. 跳转到第六步和第七步
  11. 如果是可读事件,需要来获取数据,通过buffer将数据从通道读取到buffer,然后对buffer缓冲区进行读写切换,接着将buffer上的数据进行获取,即拿到了客户端发送的数据
  12. 关闭资源socketChannel、serverSocketChannel和selector

其中:6-11步是可以不断循环进行连接和数据接收的

客户端编程:

/**
 * NIO客户端流程
 */
public class Client {
    public static void main(String[] args) {
        startClient("127.0.0.1",7779);
    }

    public static void startClient(String ip,int port) {
        try {
            //创建socketChannel通道
            SocketChannel socketChannel = SocketChannel.open();

            //将socketChannel设置为非阻塞
            socketChannel.configureBlocking(false);

            //创建Selector复用器实例
            Selector selector = Selector.open();

            //connect是一个客户端主动触发连接操作,connect操作不会阻塞,会直接返回,如果连接成功返回true,连接未完成则返回false
            if (!socketChannel.connect(new InetSocketAddress(ip,port))) {
                System.out.println("连接操作为完成,交给复用器监听事件处理");
                //当前连接操作未完成
                //将socketChannel注册到复用器,并关注可连接事件
                socketChannel.register(selector,SelectionKey.OP_CONNECT);

                //等待复用器返回结果,连接完成的结果
                selector.select();

                Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()){
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();

                    //是否是可连接事件
                    if (selectionKey.isConnectable()) {
                        //可连接事件完成
                        SocketChannel socketChannel1 = (SocketChannel) selectionKey.channel();

                        //连接操作完成
                        socketChannel1.finishConnect();
                    }
                }
            }

            System.out.println("连接服务端成功");
            //连接服务端成功,给服务端发送消息
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            //将发送的数据写入buffer
            byteBuffer.put("hello\n".getBytes());
            //buffer读写模式切换
            byteBuffer.flip();
            //将数据从buffer中写入通道
            socketChannel.write(byteBuffer);

            //关闭资源
            socketChannel.close();

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

NIO编程的客户端流程:

  1. 实例化通道socketChannel
  2. 设置socketChannel通道为非阻塞
  3. 实例化复用器selector
  4. 连接服务端:connect(当前返回不论是否连接成功会直接返回,通过Boolean类型返回值判断,连接成功返回true,连接失败返回false)
  5. 如果返回是false,表示连接未完成,将通道注册到复用器中,并监听connect事件
  6. 复用器监听事件是否完成,如果完成判断集合是否有可连接事件,即可连接事件完成socketChannel1.finishConnect();
  7. 通过buffer和channel之间的交互给服务端发送消息

实现echo命令,客户端可以多次给服务端发送数据,并接收到服务端返回的数据

服务端编程:

/**
 * NIO服务端编程
 */
public class Server {
    public static void main(String[] args) {
        startServer(7779);
    }

    public static void startServer(int port) {
        try {
            //创建ServerSocketChannel通道
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

            //绑定端口
            serverSocketChannel.bind(new InetSocketAddress(port));
            System.out.println("服务端绑定端口:"+port+"并启动啦");

            /**
             * 进行accept接收客户端连接
             * 该方法在BIO中是阻塞的,
             * 在NIO中需要设置为非阻塞,将其交给复用器管理监听事件
             */
            //将通道设置为非阻塞
            serverSocketChannel.configureBlocking(false);

            //创建selector复用器实例
            Selector selector = Selector.open();

            //将通道注册到复用器上并关注可接受事件
            serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);

            //等待复用器监听结果,通过调用select方法阻塞等待,直到有事件发生
            //循环监听复用器是否有时间准备就绪
            while (selector.select()> 0) {
                //selectedKeys 感兴趣事件的集合
                Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();
                    //判断是否是可接受事件
                    if (selectionKey.isAcceptable()) {
                        //当前有可接受事件发生,即由客户端连接
//                        System.out.println("有可接受事件发生");
                        ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();

                        //通过accept来接收客户端的连接,accept不是阻塞的了
                        SocketChannel socketChannel = serverSocketChannel1.accept();

                        System.out.println(socketChannel.getRemoteAddress()+"客户端连接上了");
                        //可以进行读写操作,需要将通道设置为非阻塞
                        socketChannel.configureBlocking(false);

                        //将socketChannel通道注册到复用器上,并关注读事件
                        socketChannel.register(selector,SelectionKey.OP_READ);
                    }

                    //判断是否是可读事件
                    if (selectionKey.isReadable()) {
//                        System.out.println("有可读事件发生");
                        //获取通道,通过通道来读取数据
                        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();

                        //创建buffer
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        //将通道数据写入buff
                        int read = socketChannel.read(byteBuffer);
                        //对buff进行读写模式切换
                        byteBuffer.flip();
                        //将数据从buff中读取出来
                        byte[] bytes = new byte[byteBuffer.remaining()];
                        byteBuffer.get(bytes);
                        String msg = new String(bytes);
                        System.out.println("客户端"+socketChannel.getRemoteAddress()+"发送数据:"+msg);

                        //返回给客户端数据
                        msg = "【echo】"+msg+"\n";
                        //复用buffer,先对buffer清空
                        byteBuffer.clear();
                        //将数据写入buffer
                        byteBuffer.put(msg.getBytes());
                        //读写模式切换
                        byteBuffer.flip();
                        //将数据从buffer写入channel
                        socketChannel.write(byteBuffer);

                        if (read == -1) {
                            System.out.println("数据接收完成结束");
                            //数据接收结束
                            socketChannel.close();
                        }
                    }
                }
            }

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

客户端编程:

/**
 * NIO客户端流程
 */
public class Client {
    public static void main(String[] args) {
        startClient("127.0.0.1",7779);
    }

    public static void startClient(String ip,int port) {
        try {
            //创建socketChannel通道
            SocketChannel socketChannel = SocketChannel.open();

            //连接服务器connect是一个阻塞函数
            socketChannel.configureBlocking(false);

            //创建Selector复用器实例
            Selector selector = Selector.open();

            //connect是一个客户端主动触发连接操作,connect操作不会阻塞,会直接返回,如果连接成功返回true,连接未完成则返回false
            if (!socketChannel.connect(new InetSocketAddress(ip,port))) {
                System.out.println("连接操作为完成,交给复用器监听事件处理");
                //当前连接操作未完成
                //将socketChannel注册到复用器,并关注可连接事件
                socketChannel.register(selector,SelectionKey.OP_CONNECT);

                //等待复用器返回结果,连接完成的结果
                selector.select();

                Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()){
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();

                    //是否是可连接事件
                    if (selectionKey.isConnectable()) {
                        //可连接事件完成
                        SocketChannel socketChannel1 = (SocketChannel) selectionKey.channel();

                        //连接操作完成
                        socketChannel1.finishConnect();
                    }
                }
            }

            System.out.println("连接服务端成功");
            //子线程发送消息
            new SendMsg(socketChannel).start();
            socketChannel.register(selector,SelectionKey.OP_READ);
            while (selector.select() > 0){
                Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();
                    if (selectionKey.isReadable()) {
                        SocketChannel channel = (SocketChannel) selectionKey.channel();
                        //将复用buffer,清空buffer
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        //将通道数据读取到buffer
                        channel.read(byteBuffer);
                        //【echo】tulun
                        //读写模式切换
                        byteBuffer.flip();
                        byte[] bytes = new byte[byteBuffer.remaining()];
                        byteBuffer.get(bytes);
                        String recv = new String(bytes);
                        System.out.println("服务端返回:"+recv);

                    }
                }
            }

            //关闭资源
            socketChannel.close();

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


public class SendMsg extends Thread {
    private SocketChannel socketChannel;
    private Scanner scanner = new Scanner(System.in);

    public SendMsg(SocketChannel socketChannel){
        this.socketChannel = socketChannel;
    }

    @Override
    public void run() {
        while (scanner.hasNext()) {
            String msg = scanner.nextLine();
            if (msg == null || "".equals(msg.trim())) continue;
            //连接服务端成功,给服务端发送消息
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            //将发送的数据写入buffer
            byteBuffer.put(msg.getBytes());
            //buffer读写模式切换
            byteBuffer.flip();
            try {
                //将数据从buffer中写入通道
                socketChannel.write(byteBuffer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

注:

问题1:客户端为什么主动connect操作?

在BIO中connect操作是一个阻塞的操作,在NIO中设置为非阻塞,交给IO复用器来监听事件是否完成(connect可连接事件),内核帮助监听事件是否完成,必须先触发事件发生,发生之后内核才能监听事件是否完成。客户端的连接操作主动连接服务端,在NIO中socketChannel设置为非阻塞,当前的connect操作会立即返回(true:连接成功,false:当前事件已经触发,但并没有连接成功,需要将连接等待的过程交给内核监听,即将未完成的连接注册到复用器上)。

read为例,假如是客户端给服务端发送消息,对于服务端read而言,需要等待数据是否完成,客户端发送的数据已经接收成功时,内核才通知服务端,这个read操作的主动触发是客户端触发的(客户端先触发,发送数据由内核等待才能等到数据,等到数据完成再通知服务端应用程序处理)

问题2:为什么客户端断开连接服务端以后,循环接口有可读事件为空?

客户端断开连接,服务端会接收-1,占用空间。服务端判断是否有数据,判断的是接收空间是否有数据,如果是则服务端就会认为一直有可读事件需要服务端处理,判断通过接收-1结束接收

问题3:为什么写事件没有注册到复用器中

写操作在NIO中也是一个事件,但写事件需要主动发起写操作,一般写完之后立即write操作不会进行阻塞,即通常写操作并不会注册到复用器中

NIO中重要组件

channel:通道

Java中NIO的channel类似流,但又不同:

  1. 通道是既可以从通道中读取数据,又可以写数据到通道,但流的读写是单向的
  2. 通道的数据总是先读到一个buffer中,或者总是要从一个buffer中写入

读数据:将数据从channel中读取到buffer,从buffer再获取数据

写数据:将数据先写入buffer,buffer中的数据写入通道

buffer流的使用示例:以读为例使用

 channel的主要实现类:

ServerSocketChannel:监听新进来的TCP连接,对于每一个连接都会创建一个新的SocketChannel,一般在服务端实现

SocketChannel:TCP的连接中使用,用于主动连接ServerSocketChannel通道,一般在客户端使用

DatagramChannel:用于UDP读写网络中的数据通道

FileChannel:用于读取、写入、映射和操作文件的通道

主要是ServerSocketChannel和SocketChannel实现C/S大致流程:

服务端:

  1. 通过ServerSocketChannel绑定端口
  2. 通过ServerSocketChannel的accept方法创建一个SocketChannel对象用于客户端数据的读取和写入
  3. 创建读/写数据的缓冲区对象来读取客户端的数据或者向客户端发送数据
  4. 关闭SocketChannel和ServerSocketChannel

客户端:

  1. 通过SocketChannel来连接connect服务端
  2. 创建读/写数据的缓冲区对象来读取服务端的数据或者向服务端发送数据
  3. 关闭SocketChannel

Buffer:缓冲区

Java中NIO的Buffer用于和channel交互,buffer在读写数据时都需要用到,本质上是可以写入数据,再从中将数据读取写入内存,buffer在java.nio包下提供了一系列的方法,方便开发者访问该内存

基本用法:

使用buffer读写数据一般需要四步:

  1. 将数据写入buffer
  2. 调用buffer.flip()进行读写模式切换
  3. 从buffer中读取数据
  4. 调用clear()清空数据

使用示例:

Buffer实现

buffer的实现底层通过特定类型(byte、long...)数组来存储数据,数组中的数据是借助4个指针来操作

private int mark = -1; //标记
private int position = 0; //位置
private int limit; //限制
private int capacity; //容量

新创建的buffer缓冲区标记、位置、限制和容量遵循不变式

mark<=position<=limit<=capacity

读写模式示意图

 capacity:

作为一个内存块,buffer有固定大小,即capacity,往buffer中写的数据最多为capacity,一旦buffer满,需将其清空才能继续往里写数据

position:

取决于buffer是读还是写模式

写数据的buffer,position表示当前写的位置,初始的position值为0,当一个byte、long等数据写到buffer后,position会向前移动到下一个可插入数据的buffer单元,所以position最大可以为capacity-1

读数据时,也是从某个特定位置读,当将buffer从写模式切换到读模式,position会被重置为0,当从buffer的position处读取数据时,position向前移动到下一个可读数据位置

limit:

写模式下,表示最多能往buffer中写入多少数据,所以limit=capacity

读模式下,limit表示最多能读取到的数据

读写模式切换,从写模式切换为读模式,limit会被设置为写模式下的position位置,即能读到之前写入的所有数据

读写实现指针变换过程

 buffer类型:

Buffer是一个抽象类,其实现的子类有ByteBuffer(字节缓冲流)、CharBuffer(字符缓冲流),Java NIO中Buffer有如下类型

 缓冲区有两种,堆上开辟的空间(在内存模型上的堆)、堆外开辟空间

对buffer的使用首先需要分配空间

以ByteBuffer为例说明Buffer的创建

ByteBuffer allocate(int capacity):在堆上开辟指定的capacity大小的缓冲

ByteBuffer allocateDirect(int capacity):在堆外空间创建指定的capacity大小的缓冲

ByteBuffer wrap(byte[] array):通过byte数组实例创建一个缓存

ByteBuffer wrap(byte[] array,int offset, int length):指定byte数组中从offset开始读取length长度的数据来实例创建一个缓存

Buffer中提供的方法;

 从buffer中读数据

两种方式:

1.从buffer中读取数据到channel

channel.write(buffer)//socketChannel.write(byteBuffer)

2.使用buffer的get方法从buffer中读取数据

buffer.get(bytes) // byteBuffer.get(bytes)

向buffer中写数据

两种方式:

1.从channel写到buffer

channel.read(buffer) //socketChannel.read(byteBuffer)

2.通过buffer的put方法协大buffer李

buffer.put(94) //byteBuffer.put(mag.getBytes)

Selector:复用器

Selector称为复用器,选择器等,作用是监听一个或者多个NIO的channel的状态是否可读、可写等。可以实现单个线程管理多个channel,也可以管理网络请求。

Selector的优势:使用更少的线程管理更多的通道,相比于多线程,减少了上下文切换

Selector的使用

1.创建Selector实例

通过调用Selector.open()创建一个Selector对象

Selector selector = Selector.open();

2.注册通道到选择器Selector上

//将通道设置为非阻塞
serverSocketChannel.configureBlocking(false);
//将通道注册到复用器上并关注可接受事件
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);

注:注册的通道是非阻塞的

SelectableChannel是抽象类,提供了configureBlocking的方法用于设置当前通道处于阻塞状态还是非阻塞状态。SocketChannel、ServerSocketChannel、DatagramChannel都是继承自SelectableChannel类,可以使用configureBlocking设置非阻塞模式

3.使用Selector选择器来监听事件是否完成

selector.select()监听事件,该方法会阻塞直至内核监听到感兴趣事件发生才会返回

4.遍历感兴趣事件集合

通过调用select.selectedKeys()返回的是SelectionKey的Set集合,通过迭代器遍历集合中的事件,当前的SelectionKey存放的是注册的已就绪的事件

Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();

5.如果还有关注事件,则跳转到第三步继续监听

6.最终关闭选择器

SelectableChannel抽象类

SelectableChannel是一个抽象类

SelectableChannel在设计时,是可以处于“阻塞”和“非阻塞”两种模式(configureBlocking方法设定),在阻塞模式下,每个IO操作完成之前,都会阻塞其他的IO操作,类似BIO的特点

在非阻塞模式下,永远不会阻塞IO操作,其将会使用Selector作为异步支持,即任何write和read都不会阻塞,可能会立即返回,新创建的SelectableChannel总是处于阻塞状态,如果需要使用Selector,那么必须使用非阻塞模式。当向某个Selector注册时,此时channel必须处于非阻塞模式,这种非阻塞设定一次就不会再改变,直到SelectionKey被销毁

SocketChannel和ServerSocketChannel都是SelectableChannel的子类

SelectableChannel类提供了register方法,用于将通道注册到选择器中,方法第二个参数,表示是通过Selector监听channel对什么事件感兴趣,可以监听四种不同类型的事件

SelectionKey.OP_READ

SelectionKey.OP_WRITE

SelectionKey.OP_ACCEPT

SelectionKey.OP_CONNECT

通道通过触发一个事件意味着该事件已经准备就绪,可以通过用户进行处理

SelectionKey介绍

SelectionKey是一个抽象类,表示了一个特定的通道对象和一个特定的选择器之间的注册关系

SelectionKey对四个事件用四个常量表示

SelectionKey.OP_READ

SelectionKey.OP_WRITE

SelectionKey.OP_ACCEPT

SelectionKey.OP_CONNECT

读操作符

public static final int OP_READ=1<<0

写操作符

public static final int OP_WRITE=1<<2

连接操作符

public static final int OP_CONNECT=1<<3

接收操作符

public static final int OP_ACCEPT=1<<4

//返回该SelectionKey对应通道

public abstract SelectableChannel channel();

//返回该SelectionKey注册的选择器

public abstract Selector selector();

//判断该SelectionKey是否有效,false:表示无效

public abstract boolean isValid()

//撤销该SelectionKey

public abstract void cancel()

//返回的是SelectionKey关注的操作符

public abstract int interestOps()

//返回的是SelectionKey的预备操作符

public abstract int readyOps()

//设置SelectionKey的附件

public final Object attach(Object ob)

Selector核心店在于选择器的监听事件

如何监听事件

Selector中维护了三种类型间 的集合

Selector维护了三种类型的SelectionKey的集合

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

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

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

已注册的键的集合的子集,这个集合的每个成员都是关联的通道被选择器(在前一个选择操作中)判断为已经准备好的,并且包含键的的interest集合中的操作,这个集合通过selecetdKeys()方法返回,并且有可能是空的

不要将已选择键的集合与ready集合混淆,这是一个键的集合,每个键都关联一个已经准备好至少一种操作的通道,每个键都有一个内嵌的ready(事件的集合)集合,包含了所关联的通道已经准备好的操作

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

已注册的键的集合的子集,这个集合包含了 cancel() 方法被调用过的键(这个键已经被无效化),

但它们还没有被注销。这个集合是选择器对象的私有成员,因而无法直接访问。

注意:

一个Selector上可以注册多个通道,而每个通道上有关注的事件(SelectionKey),一个通道有个可能关注多个事件

interest集合指的是一个通道上/键的关注的事件集合(读事件 | 写事件|连接事件|接收事件)

ready集合指的是一个通道上/键的已经准备就绪的事件集合

select的选择过程

在刚初始化的Selector对象中,这三个集合都是空的

1、已取消的键的集合将被检查。如果非空,每个被取消的键的集合将从其他两个集合中移除,并且相关通道将被注销,该步骤后,已取消集合将是空的

2、已注册的键的集合汇总的键的interest集合将被检查,该步骤的检查执行后,对interest集合的改动不会影响剩余的检查过程

一旦就绪事件被确定下来了,底层操作系统将会被查询,来确定底层操作系统真是就绪状态,依赖特定的select()方法调用,如果没有通道准备好,线程将被阻塞在这里,通常会有超时值,直到操作系统调用完成为止,这个过程可能会使调用线程睡眠一段时间,然后当前每个通道的就绪时间将被确定下来,对于那些没有准备好的通道将不执行任何操作,对于那些系统指示至少已经准备好interest集合中的一种操作的通道,将执行以下两种操作中的一种:

a.如果通道的键还没有处于已选择的键的集合中,那么键的ready集合将被清空,然后表示操作系统发现的当前通道已经准备好的操作系统的比特掩码将被设置

b.否则,也就是键在已选择的键的集合中,键的ready集合将被表示操作系统发现的当前已经准备好的操作系统的比特掩码更新。所以之前的已经不再是就绪状态的操作不会被清除,事实上,所有的比特为位都不会被清除,有操作系统决定的ready集合是与之前的ready集合按位分离的,一旦键被放置与选择器的已选择的键的集合中,他的ready集合将是积累的,比特位只会被设置,不会被清理

3、步骤2可能耗费较长,特别激发的线程处于休眠状态是,与该选择器相关的键可能被同时取消,当步骤2结束时,步骤1将重新执行,以完成任意一个在选择进行的过程中可能已经被取消的通道注销

4、select操作的返回值是ready集合在步骤2中被修改的键的数量,而不是已选择的键的集合中的通道的总数,返回值不是已准备好的通道的总数,而是从上一个select()调用之后进入就绪状态的通道的数量,之前的调用中就绪的并且在本次调用中任然就绪的通道将不会被计入,而那些在前一次的调用中已经就绪但已经不再处于就绪状态的通道也不会被计入,这些通道可能已经在已选择的键的集合中但不会被计入返回值中,返回值可能是0

使用内部的已取消的键的集合来延迟注销,是一种防止线程在取消键时阻塞,并防止与正在进行的选择操作冲突的优化,注销通道是一个代价很高的操作,这可能需要重新分配资源(记住:键是和通道相关的,并且可能与他们相关的通道之间有复杂的交互),清理已取消的键,并与选择操作之前和之后注销通道,可能消除他们之间正好选择的过程中执行的潜在棘手问题,这是另一种兼顾健壮性的折中方案

select方法介绍

select方法会返回已经就绪的通道,Selector重载了几个select方法

int select():会阻塞直至至少有一个通道在注册的事件上就绪才会返回

int select(long timeout):和select()一样,最长阻塞时间是timeout毫秒

int selectNow():非阻塞,只要有通道就绪会立即返回

select方法返回去的int表示有多少个键的ready集合准备就绪,是自上一次select方法之后有多少个准备就绪,之前在select调用时就已经就绪的通道是不会在本次中计入

停止选择的方法

通过相应的方法来唤醒在select中阻塞的线程

wakeup():通过selector调用wakeup方法会让处于阻塞状态的select立即返回,该方法使得选择器上的第一个还没有返回的选择操作会立即返回,如果当前没有进行中的选择过程,那么下一次对select()方法的调用将会立即返回

close():通过close来关闭selector实例,该方法使得任何一个在选择操作过程中阻塞的线程都会被唤醒。同时使得注册到selector上的channel会被注销,所有的键会被取消。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是使用Java NIO模型中的FileChannel实现文件复制的代码实现: ```java import java.io.FileInputStream; import java.io.FileOutputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class FileCopyDemo { public static void main(String[] args) throws Exception { FileInputStream inputStream = new FileInputStream("source.txt"); FileChannel inputChannel = inputStream.getChannel(); FileOutputStream outputStream = new FileOutputStream("target.txt"); FileChannel outputChannel = outputStream.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(1024); while (inputChannel.read(buffer) != -1) { buffer.flip(); outputChannel.write(buffer); buffer.clear(); } inputChannel.close(); outputChannel.close(); inputStream.close(); outputStream.close(); System.out.println("File copied successfully!"); } } ``` 以上代码中,我们首先定义了一个FileInputStream对象,并通过调用getChannel()方法创建一个FileChannel对象来进行文件读取。接着我们定义了一个FileOutputStream对象,并通过调用getChannel()方法创建一个FileChannel对象来进行文件写入。 然后我们使用allocate()方法创建一个1024字节大小的ByteBuffer对象。在while循环中,我们不断从inputChannel中读取数据到buffer中,然后将buffer的position设置为0,limit设置为buffer的当前position,接着通过write()方法将buffer中的数据写入到outputChannel中。 最后,我们需要关闭输入输出流和FileChannel对象,并输出文件复制成功的提示信息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值