zoukankan      html  css  js  c++  java
  • JAVA技术——NIO详解

    JAVA技术——NIO详解

    一、概述

    在了解NIO之前,先解释几个关键词

    同步与异步:

        同步:同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步。

        简单理解,就好像是,你在淘宝上看到一件商品,选择了购买,当你选择了购买之后,你的页面会一直处于等待当中,直到商家确定了订单,返回了相信,页面才会挑战到,购买成功页面,这就是同步。

        异步:异步正好相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系。用异步去淘宝买东西,就是看中了什么直接购买,页面当即跳转显示购买成功,商家什么时候看到订单,什么时候返回消息,你不用等待,可以去干别的。

    阻塞与非租塞:

        阻塞:在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如serversocket新连接建立完成,或者数据读取、写入操作完成;

       非阻塞:不管IO操作是否结束,直接返回,相应操作在后台继续处理

    看到这里是不是觉得这不说的一件事吗?不错,我当初也有这种疑惑,但是同步和阻塞异步和则塞又确实存在一些差别。这里推荐一篇文章有兴趣的同学可以去看看,这里只贴结论;

      地址:

    https://blog.csdn.net/lengxiao1993/article/details/78154467?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.add_param_isCf&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.add_param_isCf

      结论:

    1. 阻塞/非阻塞, 同步/异步的概念要注意讨论的上下文:
    • 在进程通信层面, 阻塞/非阻塞, 同步/异步基本是同义词, 但是需要注意区分讨论的对象是发送方还是接收方。

      • 发送方阻塞/非阻塞(同步/异步)和接收方的阻塞/非阻塞(同步/异步) 是互不影响的。
    • 在 IO 系统调用层面( IO system call )层面, 非阻塞IO 系统调用 和 异步IO 系统调用存在着一定的差别, 它们都不会阻塞进程, 但是返回结果的方式和内容有所差别, 但是都属于非阻塞系统调用( non-blocing system call )

    1. 非阻塞系统调用(non-blocking I/O system call 与 asynchronous I/O system call) 的存在可以用来实现线程级别的 I/O 并发, 与通过多进程实现的 I/O 并发相比可以减少内存消耗以及进程切换的开销。
    2. 如果简单来看也可以这么理解:同步是发起了一个调用后, 没有得到结果之前不返回,但是并没有被阻塞,在数据准备阶段轮询间隙中是可以执行其它任务的。而阻塞是完全不能执行其他任务了。

    二、BIO、NIO、AIO三者简述。

    首先这三者都是IO流,但是都有各自的特性,应对不同的场景。我们前面对IO流的简单操作,都可以看成是BIO。

    BIO,即同步阻塞IO,也就是干完一件事,再去干别的事。这种IO简单,但是效率低下。

    JDK1.4之后出来了NIO,即同步非阻塞,也就是这个线程依然要等待返回结果,但是可以去干点别的事,不用一直在这等着了。

    JDK1.7之后又出了NIO2.0也就是AIO,这就是异步非阻塞,即这个线程连结果都不等待了,直接返回干别的事,返回结果操作系统会通知相应的线程来进行后续的操作。

    三、NIO相对于BIO的优势。

    1、NIO是一块的方式处理数据,但是IO是以最基础的字节流的形式去写入和读出的,效率上NIO要高出很多

    2、NIO不再是和IO一样用OutputStream和InputStream输入流的形式来进行处理数据的。但是又是基于这种流的形式,采用了通道和缓冲区的形式来进行处理数据的。

    3、还有一点就是NIO的通道是可以双向的,但是IO中的流只能是单向的。

    4、NIO的缓冲区还可以进行分片,可以建立只读缓冲区、直接缓冲区、和简介缓冲区。也就是说NIO面向的是缓冲区,IO是面向的流

    5、NIO采用的是多路复用的IO模型,普通的IO用的是阻塞的IO模型。

    小结:NIO的三个部分分别是  Buffer、Channel、Selector是NIO的三个部分。NIO是在访问个数特别大的时候要用的。比如流行的软件或流行的游戏中会有高并发和大量连接。

    3.1、buffer

    buffer可以对基本类型的数组进行封装。基本类型的数组不属于类不能调用的任何的方法,但是buffer是一个类,里面包含了很多的方法。

    ByteBuffer (最重要的一个)
    CharBuffer
    DoubleBuffer
    FloatBuffer
    IntBuffer
    LongBuffer
    ShortBuffer

    3.2、ByteBuffer的创建方式

    ByteBuffer内部封装了一个byte[]数组,ByteBuffer里面有一些方法可以对数组进行操作。

    • 在堆中创建缓冲区:allocate(int capacity)

    • 在系统内存创建缓冲区:allocatDirect(int capacity)

    • 通过数组创建缓冲区:wrap(byte[] arr)

    3.3、常用方法

    put(byte b) :给数组添加元素。

    capacity() :获取容量。容量就是数组的长度,不会改变。

    limit() : 限制。

    • 限制就是可以指定一个索引,限制所指向索引以及后面的索引不能操作。

    • 不加参数表示获取当前的限制,加参数表示设置一个限制

    public class Demo04常用方法 {
        public static void main(String[] args) {
            //创建对象
            ByteBuffer buffer = ByteBuffer.allocate(10);
    
            //获取限制
            int i = buffer.limit();
            System.out.println(i);    //10
    
            //添加元素
            buffer.put((byte)3);
            buffer.put((byte)3);
            buffer.put((byte)3);
    
            //设置限制
            //从4索引开始不允许存放数据了
            buffer.limit(4); 
    
            buffer.put((byte)11);
            buffer.put((byte)22);	//在这里会报错
            buffer.put((byte)33);
        }
    }
    

      

    position() :位置,指向将要存放元素的位置,每次存放元素位置会向后移动一次。

    • 不加参数表示获取当前位置,加参数表示设置位置

    • position位置变量默认只会向前走,不会倒退

    public class Demo05常用方法 {
        public static void main(String[] args) {
            //创建对象
            ByteBuffer buffer = ByteBuffer.allocate(10);
    
            //获取位置(位置就是要存放元素的位置)
            int i = buffer.position();
            System.out.println(i);    //0
            //添加元素
            buffer.put((byte)11);
            //再次获取
            i = buffer.position();
            System.out.println(i);  //1
            //设置位置
            //把position指向6索引
            buffer.position(6);
            //添加元素
            buffer.put((byte)22);
            buffer.put((byte)33);
    
            System.out.println(Arrays.toString(buffer.array()));
            //[11, 0, 0, 0, 0, 0, 22, 33, 0, 0]
        }
    }
    

      

    mark() : 标记。

    • 把当前位置设置为标记,当调用reset()重置时,position会回到mark标记的地方。

    public class Demo06常用方法 {
        public static void main(String[] args) {
            //创建对象
            ByteBuffer buffer = ByteBuffer.allocate(10);
            //mark标记  标记就是指向一个索引,当调用reset()方法时,position会回到标记的位置
    
            //添加方法
            buffer.put((byte)11);
            buffer.put((byte)22);
            buffer.put((byte)33);
            buffer.put((byte)44);
    
            //标记  把当前position标记,mark=4
            buffer.mark();
    
            buffer.put((byte)55);
            buffer.put((byte)66);
            buffer.put((byte)77);
    
            //reset()重置
            buffer.reset();
    
            buffer.put((byte)88);
    
            //打印
            System.out.println(Arrays.toString(buffer.array()));
            //[11, 22, 33, 44, 88, 66, 77, 0, 0, 0]
        }
    }

    clear():还原数组的状态。

    • 将position设置为:0

    • 将限制limit设置为容量capacity

    • 丢弃标记mark

    flip():切换。在读和写中间会调用这个方法。

    • 将limit设置为当前position位置

    • 将当前position位置设置为0

    • 丢弃mark标记

    public class Demo08常用方法 {
        public static void main(String[] args) {
            //创建对象
            ByteBuffer buffer = ByteBuffer.allocate(10);
    
            //添加元素
            buffer.put((byte)11);
            buffer.put((byte)22);
    
            System.out.println(buffer);   //pos=2 lim=10 cap=10
    
            //切换
            //flip()
            //- 将limit设置为当前position位置
            //- 将当前position位置设置为0
            //- 丢弃mark标记
            buffer.flip();
    
            System.out.println(buffer);  //pos=0 lim=2 cap=10
    
        }
    }
    其实可以想象,你在打字,打到一半一执行这个方法,就停在这里不能再打字了,只能看以前写的那个了,所以说常用在读和写之间。

     3.3、channel通道 

    Channel表示通道,可以去做读取和写入的操作。相当有我们之前学过的IO流。Channel是双向的,一个对象既可以调用读取的方法也可以调用写出的方法。

    Channel在读取和写出的时候,要使用ByteBuffer作为缓冲数组。

    3.3.2、分类

    • FileChannel:从文件读取数据的

    • DatagramChannel:读写UDP网络协议数据

    • SocketChannel:读写TCP网络协议数据

    • ServerSocketChannel:可以监听TCP连接

    3.3.3、FileChannel基本使用

    FileChanner是对文件进行读写的通道,可以理解为之前IO流。

    使用FileChanner完成文件的复制

    public class DemoFileChannel完成文件复制 {
        public static void main(String[] args) throws  Exception{
            //创建输入流
            FileInputStream fis = new FileInputStream("day19\aaa\123.txt");
            //创建输出流
            FileOutputStream fos = new FileOutputStream("C:\Users\jin\Desktop\123.txt");
    
            //IO流都有方法getChannel可以获取通道
            FileChannel c1 = fis.getChannel();
            FileChannel c2 = fos.getChannel();
    
            //创建数组
            ByteBuffer buffer = ByteBuffer.allocate(1024);
    
            //文件复制
            while (c1.read(buffer) != -1){  
                //切换
                buffer.flip(); //可以简单理解为指针指向开头,从头开始输出,因为一打开文件,指针指向的是最后一个文字。
                //输出数据
                c2.write(buffer);
                //清空
                buffer.clear();//为什么要掉一下clear,因为不掉clear会成为死循环,当数据输出完了后,他并没有找到-1,clean清除缓存区,就看到-1了。
    
            }
    
            //关闭资源
            c1.close();
            c2.close();
            fis.close();
            fos.close();
        }
    }
    

      

    FileChannel结合MappedByteBuffer实现高效读写

    MappedByteBuffer是ByteBuffer的子类。他可以完成高效读写。能够把硬盘中的数据映射到内存中。

    把硬盘中的读写变成内存中的读写。

    public class Demo01_2G以下文件读写 {
        public static void main(String[] args) throws IOException {
            //C:资料小资料文件设置加密.avi
            RandomAccessFile f1 = new RandomAccessFile("C:\资料\小资料\文件设置加密.avi","r");
            RandomAccessFile f2 = new RandomAccessFile("C:\Users\jin\Desktop\复制.avi","rw");
    
            //获取通道
            FileChannel c1 = f1.getChannel();
            FileChannel c2 = f2.getChannel();
    
            ////获取文件的大小
            long size = c1.size();
    
            //映射
            //第一个参数是:操作方式,第二个参数:从哪儿开始,第三个参数:个数
            MappedByteBuffer buffer1 = c1.map(FileChannel.MapMode.READ_ONLY, 0, size);
            MappedByteBuffer buffer2 = c2.map(FileChannel.MapMode.READ_WRITE, 0, size);
    
            //读写
            for(int i=0; i<size; i++){
                //从第一个数组中获取数据
                byte b = buffer1.get();
                //放到第二个数组中
                buffer2.put(b);
            }
    
            //关闭资源
            c1.close();
            c2.close();
            f2.close();
            f1.close();
    
        }
    }
    

      2G以上的文件读写

     public static void main(String[] args) throws IOException {
            //源文件
            RandomAccessFile f1 = new RandomAccessFile("C:\Java课件\1_第一阶段.zip","r");
            //目标文件
            RandomAccessFile f2 = new RandomAccessFile("C:\Users\jin\Desktop\复制.zip","rw");
    
            //获取通道
            FileChannel c1 = f1.getChannel();
            FileChannel c2 = f2.getChannel();
    
            //获取文件大小
            long size = c1.size();
            //每块期望大小
            long every = 500 * 1024 * 1024;
            //块数
            int count = (int)Math.ceil(size*1.0/every);
    
            //循环
            for (int i = 0; i < count; i++) {
                //每次复制开始位置
                long start = every*i;
                //每次复制实际大小
                long trueSize = size-start<every ? size-start : every;
                //映射
                MappedByteBuffer m1 = c1.map(FileChannel.MapMode.READ_ONLY, start, trueSize);
                MappedByteBuffer m2 = c2.map(FileChannel.MapMode.READ_WRITE, start, trueSize);
    
                //把m1数组中内容复制到m2数组中
                for (long l = 0; l < trueSize; l++) {
                    byte b = m1.get();
                    m2.put(b);
                }
    
            }
    
            //关闭资源
            c1.close();
            c2.close();
            f2.close();
            f1.close();
        }
    }
    

     

    4.网络编程收发信息

    TCP网络编程的通道,SocketChannel代替之前的Socket,ServerSocketChannel代替之前的ServerSocket

    SocketChannel客户端通道
    ServerSocketChannel服务器通道
    设置非阻塞

    之前的accept是阻塞的方法,如果连接不到客户端就一直等着。

    在NIO中可以设置为非阻塞,设置非阻塞之后,就不会在accept()方法上一直停留。

    设置方式:


    ServerSocketChannel中方法:
    configureBlocking(false);//false代表非阻塞

    五.Selector选择器

    5.1.多路复用的概念

    在高并发的情况下,客户端很多,服务器如果有很多线程就造成服务器压力。多路复用意思是让一个Selector去管理多个端口。

    5.2.Selector方法详解

    Selector是一个选择器,可以用一个线程处理多个事件。可以注册到多个ServerSocketChannel上,帮多个ServerSocketChannel去处理事件。用一个线程处理了之前多个线程的事务,就给系统减轻负担,提高效率。

    select() :Selector的监听方法,可以帮多个端口监听客户端。
    阻塞问题:
    1.如果没有客户端连接,select()是阻塞状态
    2.如果有客户端连接且没有处理,select()就变成非阻塞状态
    3.如果已经处理了客户端,select()就会重新进入阻塞状态

    selectedKeys() :返回一个Set集合,被连接的服务器对象会被放在集合中。

    keys() :返回一个Set集合,所有的ServerSocketChannel对象都在这个集合中。

    5.3.Selector管理的一个问题

    Selector把被连接服务器对象放在Set集合中,但是用完之后没有从集合中删除。导致被用过的对象再次使用就出现空指针异常。

    解决办法:使用迭代器的删除方法,每次用完对象就使用迭代器从把对象集合中删除。

  • 相关阅读:
    ubuntu16下点亮C170摄像头的一波三折
    看完这张图,开源协议门清
    Qt调试信息重定向输出(qInstallMessageHandler)
    C++专业术语
    vim 复制 单个 单词: 移动光标到单词词首,快速摁 yw
    讲真的
    bcp文件, 逗号文件
    缩写: i = i + 1 和 i += 1,可以看做是 i 自加的值 是1。
    $identify 的 “identify” 表示一个Perl标识符,即 identifier
    第八章: 以正则表达式进行匹配
  • 原文地址:https://www.cnblogs.com/gushiye/p/13899393.html
Copyright © 2011-2022 走看看