(五)非阻塞式编程NIO

1.NIO概述

Channel与Stream的不同:

  • Stream是具有方向性的,有输入流 or 输出流;Channel是双向的,既可以读又可以写。
  • Stream的读和写都是阻塞式的;但是Channel有两种模式,既可以阻塞式读写,又可以非阻塞式读写

  • 非阻塞的意思是,例如想从某个Channel中读取数据,但是调用读取的瞬间Channel上还没有可以被读的数据,由于非阻塞,那么调用马上就返回了。所以,为了读到某个Channel上的数据,需要不停的询问它,因此我们使用Selector。

Note:如果一个任务单线程就可以执行,那么往往比使用多线程效率要高,NIO就是一个例子。多线程不一定更有效率,因为:

  • 如果需要处理线程的数量多于CPU处理器的数量,会出现“上下文交换”。CPU的每一次切换都需要先保存当前线程的状态,之后重新执行该线程时,要加载原先线程的状态。在各个线程发生的交换过程,需要消耗系统资源;
  • 每创建一个线程,系统都要为其分配一定的系统资源。

2.Buffer简析

 

3.Channel简析

 

4.使用BIO和NIO实战:本地文件拷贝

interface FileCopyRunner {
    void copyFile(File source, File target);
}

public class FileCopyDemo {
    private static final int ROUNDS = 3; // 每种方法都运行3次来评估性能

    // 执行不同方法的函数,并评估性能
    private static void benchmark(FileCopyRunner test, File source, File target) {
        long elapsed = 0L; // 总时间
        for (int i = 0; i < ROUNDS; i++) {
            long startTime = System.currentTimeMillis();
            test.copyFile(source, target);
            elapsed += System.currentTimeMillis() - startTime;
            if (i != ROUNDS - 1) {
                target.delete(); // 每次拷贝后再删除
            }
        }
        System.out.println(test + ":" + elapsed / ROUNDS);
    }

    // 关闭各种实现了Closeable接口的资源
    private static void colse(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {

        // 一、不使用缓冲的流的拷贝(一个字节一个字节地拷贝)
        FileCopyRunner noBufferStreamCopy = new FileCopyRunner() {
            @Override
            public void copyFile(File source, File target) {
                InputStream fin = null;
                OutputStream fout = null;
                try {
                    fin = new FileInputStream(source);
                    fout = new FileOutputStream(target);

                    int result;
                    try {
                        // 如果读到数据的结尾时,会返回-1
                        while ((result = fin.read()) != -1) {
                            // 每读到一个字节,就把字节写到文件输出流中
                            fout.write(result);
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } finally {
                    colse(fin);
                    colse(fout);
                }
            }

            @Override
            public String toString() {
                return "noBufferStreamCopy";
            }
        };

        // 二、使用缓冲区的流的拷贝
        FileCopyRunner bufferedStreamCopy = new FileCopyRunner() {
            @Override
            public void copyFile(File source, File target) {
                InputStream fin = null;
                OutputStream fout = null;
                try {
                    fin = new BufferedInputStream(new FileInputStream(source));
                    fout = new BufferedOutputStream(new FileOutputStream(target));

                    // 缓冲区大小可以自己定义,例如定义为1024字节.
                    byte[] buffer = new byte[1024];

                    int result;
                    while ((result = fin.read(buffer)) != -1) {
                        // 一次可读1024个字节,而不是1个字节了. result返回当前从buffer中读取的字节数
                        fout.write(buffer,0,result);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    colse(fin);
                    colse(fout);
                }
            }

            @Override
            public String toString() {
                return "bufferedStreamCopy";
            }
        };

        // 三、channel与buffer做数据交换 nio
        FileCopyRunner nioBufferCopy = new FileCopyRunner() {
            @Override
            public void copyFile(File source, File target) {
                FileChannel fin = null;
                FileChannel fout = null;

                // 由文件得到对应的文件通道
                try {
                    fin = new FileInputStream(source).getChannel();
                    fout = new FileOutputStream(target).getChannel();

                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    // 把数据从通道中读入到缓冲区
                    while ((fin.read(buffer)) != -1) {
                        // 把buffer从写模式切换到读模式,内部通过调整position指针和limit指针来实现
                        buffer.flip();
                        // 加循环的作用:为了保证把buffer中所有的数据都读取到目标文件通道中
                        while (buffer.hasRemaining()) {
                            fout.write(buffer);
                        }
                        // 读模式调整为写模式,内部通过使position指针回到初始位置,limit回到最远端
                        buffer.clear();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    colse(fin);
                    colse(fout);
                }
            }

            @Override
            public String toString() {
                return "nioBufferCopy";
            }
        };

        // 四、通道与通道间传输数据 nio
        FileCopyRunner nioTransferCopy = new FileCopyRunner() {
            @Override
            public void copyFile(File source, File target) {
                FileChannel fin = null;
                FileChannel fout = null;
                try {
                    fin = new FileInputStream(source).getChannel();
                    fout = new FileOutputStream(target).getChannel();

                    long transfered = 0L; // 目前为止已经拷贝了多少字节的数据
                    long size = fin.size();  // 要复制文件的大小
                    // transferTo()函数不能保证把原通道所有数据都传输到目标通道
                    while (transfered != size) {
                        transfered += fin.transferTo(0, size, fout); // 第二个参数是要传输多少数据
                    }
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }finally {
                    colse(fin);
                    colse(fout);
                }
            }

            @Override
            public String toString() {
                return "nioTransferCopy";
            }
        };


        File source = new File("F:\test\project.zip");
        File target1 = new File("F:\test\p1.zip");
        File target2 = new File("F:\test\p2.zip");
        File target3 = new File("F:\test\p3.zip");
        File target4 = new File("F:\test\p4.zip");

        benchmark(noBufferStreamCopy, source, target1);
        benchmark(bufferedStreamCopy, source, target2);
        benchmark(nioBufferCopy, source, target3);
        benchmark(nioTransferCopy, source, target4);
    }
}

5.实验结果

  • 以复制大小为2.75MB的文件为例,一个字节一个字节地拷贝实在是慢的可怕。。

6.Selector简析

  • 作用:不停地“询问”通道什么时候处于可操作状态,即监听多个通道的状态。

 

  •  select()返回的数值表示目前有几个Channel处于可操作状态。
  • 目的是,跟Selector这一个对象互动,就可以得到Selector所监听的多个Channel对象状态的改变,由此实现进一步的业务逻辑。
  • SelectionKey的理解:每一个在Selector上注册的Channel,都相当于对应一个独特的“ID”,就是SelectionKey
原文地址:https://www.cnblogs.com/HuangYJ/p/14454094.html