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集合中,但是用完之后没有从集合中删除。导致被用过的对象再次使用就出现空指针异常。

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

原文地址:https://www.cnblogs.com/gushiye/p/13899393.html