NIO基础知识点整理---selector除外

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NIO基础知识点整理---selector除外相关的知识,希望对你有一定的参考价值。


JVM读取数据模型


程序执行效率更多是由I/O效率决定的

需要等待数据传输

是JVM在I/O方面的效率不足,导致程序效率降低。在操作系统中,可以从硬件上直接读取大块的数据,而JVM的I/O更喜欢小块的数据读取,相当于操作系统视同大卡车运来很多数据,JVM的I/O就喜欢一铲子一铲子的加工这些数据。

在JDK4中引入了NIO,可以最大限度的满足Java程序I/O的需求

  • java.nio包,定义了各种与Buffer相关的类.
  • java.nio.channel包,包含了与Channel和Selector相关的类.
  • java.nio.charset包,与字符集相关的类

在NIO中有三大核心组件:Channel,Buffer,Selector


NIO是什么

传统的IO是面向流的,每次可以从流中读取一个或多个自己,只能向后读取,不能向前移动,NIO是面向缓冲区的,把数据读取到一个缓冲区中,可以在缓冲区中向前/向后移动,增加了程序的灵活性。

在NIO中,所有的数据都需要通过Channel传输,通道可以直接将一块数据映射到内存中,Channel是双向的,不仅可以读取数据。还可以保存数据。程序不能直接读取Channel通道,Channel只与Buffer缓冲区进行交互。

IO流时线程阻塞的,在调用read()/write()读写数据时,线程阻塞,直到数据读取完毕或者数据完全写入,在读写过程中,线程不能做其他的任务。

NIO不是线程阻塞的,当线程从Channel中读取数据时,如果通道中没有可用的数据,线程不阻塞,可用做其他的任务


Buffer

buffer属性

Buffer缓冲区实际上就是一个数组,把数组的内容与信息包装成一个Buffer对象,它提供了一组访问这些信息的方法

缓冲区的重要属性:

1.capacity容量: 指缓冲区可用存储多少个数据,容量在创建Buffer缓冲区时指定大小,创建后不能再修改,如果缓冲区满了,需要清空后才能继续写数据。

2.position表示当前位置,即缓冲区写入/读取的位置,刚刚创建Buffer对象后,position初始化为0,写入一个数据,position就向后面移动一个单元,它的最大值是capacity-1.

当Buffer从写模式切换到读模式,position会被重置为0,从Buffer的开始位置读取数据,每读一个数据,position就向后面移动一个单元.

2.limit上限: 指第一个不能被读出或写入的位置.limit上限后面的单元既不能读也不能写,在Buffer缓冲区的写模式下,limit表示能够写入多少个数据;在读取模式下,limit表示最多可以读取多少个数据。

3.mark标记:设置一个标记位置,可以调用mark方法,把标记设置在position位置,当调用reset()方法时,就把position设置为mark标记的位置。

0<=mark<=position<=limit<=capacity


Buffer常用API

在NIO中关键的Buffer有:ByteBuffer,CharBuffet,DoubleBuffer,FloatBuffer,IntBufer,LongBuffer,ShortBuffer。

这些Buffer覆盖了能够通过I/O发送的所有基本类型:byte,char,double,flaot.int.long,short等。实际上使用较多的是ByteBuffer和CharBuffer。

  • 每个缓冲区都有一个静态方法allocate(capacity)可以创建一个指定容量的缓冲区;
  • put()方法用于向缓冲区中存储数据;
  • get()方法可以从缓冲区中读取数据;
  • 当缓冲区中还有没有读完的数据,可以调用compact方法进行压缩,将所有未读取的数据复制到Buffer的起始位置,把position设置到最后一个未读元素的后面.limit属性设置为capacity.
  • capacity()方法返回缓冲区的大小
  • hasRemaining()判断当前position后面是否还有可以被处理的数据,即判断position与limit之间是否还有数据可处理。
  • limit()返回limit上限的位置
  • mark()设置缓冲区的标志位置,这个值只能在0–position之间,以后可以通过reset()将position置为mark标记的位置.
  • position()可以返回position当前位置
  • remaining()返回当前position位置与limit之间的数据量
  • reset()方法可以将position设置为mark标记位
  • rewind():将position设置为0,取消mark标志位
  • clear()清空缓冲区,仅仅是修改position标志位0,设置limit为capacity,缓冲区中数据还是存在的
  • flip()方法可以把缓冲区由写模式切换到读取模式,先把limit设置position位置,再把position设置为0;

Buffer的API使用演示


public class BufferAPI

    public static void main(String[] args) 
        //使用字符缓冲区--一个中文字符和英文字符都是一个字符大小,只是所占字节大小不一样
        CharBuffer charBuffer=CharBuffer.allocate(12);
        curPosition(charBuffer);//position=0,limit=12

        //向缓冲区中存储数据
        charBuffer.put("大");
        charBuffer.put("忽");
        charBuffer.put("悠");
        curPosition(charBuffer);//position=3,limit=12

        //调用flip,把缓冲区从写模式切换为读取模式
        charBuffer.flip();
        curPosition(charBuffer);//position=0,limit=3

        //从缓冲区读取数据
        System.out.print(charBuffer.get());
        System.out.print(charBuffer.get());
        System.out.print(charBuffer.get());
        curPosition(charBuffer);//position=3,limit=3

        charBuffer.clear();
        curPosition(charBuffer);//position=0,limit=12,mark=-1

        //往缓冲区中放入数据
        charBuffer.put("小");
        charBuffer.put("朋");
        charBuffer.put("友");
        curPosition(charBuffer);//position=3,limit=12

        //切换为写模式
        charBuffer.flip();//position=0,limit=3,mark=-1

        //设置标记
        charBuffer.mark();//mark=0

        //再读取一个字符
        System.out.println(charBuffer.get());
        curPosition(charBuffer);//position=1,limit=3


        //调用reset()将position重置为mark标记位置
        charBuffer.reset();
        curPosition(charBuffer);//position=0,limit=3

        //调用compact,将没有读取的数据复制到position为0的位置
        charBuffer.compact();
        curPosition(charBuffer);

        //调用clear清空,只是将position=0,limit=capacity,数据依然存在
        charBuffer.clear();
        curPosition(charBuffer);

        //读取数据
        System.out.println(charBuffer);
        curPosition(charBuffer);

        //挨个读取--position与limit之间内容逐个打印
        while(charBuffer.hasRemaining())
        
            System.out.println(charBuffer.get());
        
        curPosition(charBuffer);

    

    //打印当前位置的具体信息
    public static void curPosition(CharBuffer charBuffer)
    
        System.out.println("当前缓冲区容量capacity: "+charBuffer.capacity()
                +" 当前缓冲区limit位置: "+charBuffer.limit()+
                " 当前缓冲区position指针位置: "+charBuffer.position());
    

输出结果:

当前缓冲区容量capacity: 12 当前缓冲区limit位置: 12 当前缓冲区position指针位置: 0
当前缓冲区容量capacity: 12 当前缓冲区limit位置: 12 当前缓冲区position指针位置: 3
当前缓冲区容量capacity: 12 当前缓冲区limit位置: 3 当前缓冲区position指针位置: 0
大忽悠当前缓冲区容量capacity: 12 当前缓冲区limit位置: 3 当前缓冲区position指针位置: 3
当前缓冲区容量capacity: 12 当前缓冲区limit位置: 12 当前缓冲区position指针位置: 0
当前缓冲区容量capacity: 12 当前缓冲区limit位置: 12 当前缓冲区position指针位置: 3
小
当前缓冲区容量capacity: 12 当前缓冲区limit位置: 3 当前缓冲区position指针位置: 1
当前缓冲区容量capacity: 12 当前缓冲区limit位置: 3 当前缓冲区position指针位置: 0
当前缓冲区容量capacity: 12 当前缓冲区limit位置: 12 当前缓冲区position指针位置: 3
当前缓冲区容量capacity: 12 当前缓冲区limit位置: 12 当前缓冲区position指针位置: 0
小朋友         
当前缓冲区容量capacity: 12 当前缓冲区limit位置: 12 当前缓冲区position指针位置: 0
小
朋
友
 
 
 
 
 
 
 
 
 
当前缓冲区容量capacity: 12 当前缓冲区limit位置: 12 当前缓冲区position指针位置: 12


缓冲区批量数据传输

使用缓冲区就是为了提高数据传输效率,一次读写一个字符或者一个字节效率并不高,可以进行批量的操作,可以借助于数组,把缓冲区中的一块数据读到数组中,也可以把数组中的部分内容保存到缓冲区中

public class BufferAPI

    public static void main(String[] args) 
        //使用字符缓冲区--一个中文字符和英文字符都是一个字符大小,只是所占字节大小不一样
        CharBuffer charBuffer=CharBuffer.allocate(12);
        //将字符串保存到buffer缓冲区
        charBuffer.put("123456654321");
        //反转读写模式
        charBuffer.flip();//position=0,limit=12
        System.out.println(charBuffer);//只会输出position到limit的内容

        //通过字符数组,来接收缓冲区里面的数据
        char[] dst=new char[8];
        //调用get()方法把缓冲区中的数据读到字符数组中
        //注意批量传输时大小总是固定的,如果没有指定传输的大小,意味着把数组填满
        CharBuffer charBuffer1 = charBuffer.get(dst);
        System.out.println(new String(dst));
        //get方法返回的缓冲区和最开始定义的是同一个
        System.out.println(charBuffer);
        System.out.println(charBuffer1);//缓冲区剩余的数据

        //继续把buf缓冲区的内容读到字符数组中
        //当缓冲区的数据量不足以填满整个数组时,会抛出异常
         //charBuffer.get(dst);//BufferUnderflowException

        //在批量读取缓冲区数据时,记得查询缓冲区的剩余量
        //把小缓冲区的数据填充到大的数组时,要指定缓冲区剩余量的长度
        //把buf缓冲区中剩余的数据传输到dst数组的0开始的位置
      charBuffer.get(dst,0,charBuffer.remaining());
        System.out.println(dst);

        //循环读取缓冲区中的数据
        charBuffer.clear();
        dst=new char[8];
        while(charBuffer.hasRemaining())
        
            int min = Math.min(dst.length, charBuffer.remaining());
            charBuffer.get(dst,0,min);
            System.out.println(dst);
        

        curPosition(charBuffer);
        //向缓冲区存入数据的时候,需要注意缓冲区空间不足的情况,否则会抛出异常
        //调整position指针的位置
        charBuffer.position(10);
        //此时还可以存放两个字符
        char[] test='a','b','c','d';
        charBuffer.put(test,0,charBuffer.remaining());
        charBuffer.clear();
        System.out.println(charBuffer);
    

    //打印当前位置的具体信息
    public static void curPosition(CharBuffer charBuffer)
    
        System.out.println("capacity: "+charBuffer.capacity()
                +" limit: "+charBuffer.limit()+
                " position: "+charBuffer.position());
    


缓冲区创建的两种方式

1.分配创建缓冲区,allocate()方法分配一个私有的,指定容量大小的数组来存储元素

2.包装操作创建缓冲区,它使用提供的数组作为存储空间来存储缓冲区中的数据,不再分配其他空间

public class BufferAPI

    public static void main(String[] args) 
        //缓冲区创建的两种方式
        //1.allocate()
        CharBuffer charBuffer1=CharBuffer.allocate(12);
        //2.wrap()
        CharBuffer charBuffer3=CharBuffer.wrap("大忽悠");
        curPosition(charBuffer3);

        char[] array=new char[12];
        CharBuffer charBuffer2=CharBuffer.wrap(array);
        curPosition(charBuffer2);
        
    

    //打印当前位置的具体信息
    public static void curPosition(CharBuffer charBuffer)
    
        System.out.println("capacity: "+charBuffer.capacity()
                +" limit: "+charBuffer.limit()+
                " position: "+charBuffer.position());
    

调用put方法对向缓冲区中放入数据会直接影响到数组,同样对数组做任何修改也会直接影响到缓冲区对象

        char[] array=new char[12];
        CharBuffer charBuffer2=CharBuffer.wrap(array);
        curPosition(charBuffer2);

        charBuffer2.put("嘿嘿嘿");
        System.out.println(array);
       curPosition(charBuffer2);

        array[4]='小';
        array[5]='朋';
        array[6]='友';
        System.out.println(charBuffer2);
        curPosition(charBuffer2);


hasArray()方法判断是否有一个可存取的备份数组

如果hasArray()返回true,可以通过array()返回缓冲区对象使用的备份数组的引用

     char[] array=new char[12];
        CharBuffer charBuffer2=CharBuffer.wrap(array);
        charBuffer2.put("嘿嘿嘿");

        if(charBuffer2.hasArray())
        
            char[] array1 = charBuffer2.array();
            System.out.println(array1);
        


缓冲区的复制与分隔

复制操作

public class BufferAPI

    public static void main(String[] args)
    
       CharBuffer charBuffer=CharBuffer.wrap("123456");
       //缓冲区的复制
        CharBuffer duplicate = charBuffer.duplicate();
        System.out.println(duplicate);
        //下面可以看出两个缓冲区只用数组是同一个,两个缓冲区对象是不同的对象
        //两个缓冲区实际上引用的是同一个数组
        duplicate.position(4);
        curPosition(duplicate);
        curPosition(charBuffer);
    

    //打印当前位置的具体信息
    public static void curPosition(CharBuffer charBuffer)
    
        System.out.println("capacity: "+charBuffer.capacity()
                +" limit: "+charBuffer.limit()+
                " position: "+charBuffer.position());
    



缓冲区的复制,本质是两个不同的缓冲区对象共用一个缓冲数组


分隔操作

分隔缓冲区,slice()方法根据[position.limit)区间创建一个新的缓冲区

public class BufferAPI

    public static void main(String[] args)
    
       CharBuffer charBuffer=CharBuffer.wrap("123456");
       charBuffer.position(5);
        CharBuffer slice = charBuffer.slice();
        curPosition(charBuffer);
        curPosition(slice);
    

    //打印当前位置的具体信息
    public static void curPosition(CharBuffer charBuffer)
    
        System.out.println("capacity: "+charBuffer.capacity()
                +" limit: "+charBuffer.limit()+
                " position: "+charBuffer.position());
    


直接字节缓冲区

在硬盘中和操作系统处理的数据都是01二进制,缓冲区中只有ByteBuffer字节缓冲区由资格参与I/O操作。

Channel通道只能使用ByteBuffer作为它的参数。

直接字节缓冲区通常是I/O操作最好的选择,如果使用非直接字节缓冲区可能会导致性能损耗,如果向通道传递一个非直接字节缓冲区,通道可能会先创建一个临时的直接字节缓冲区,将非直接缓冲区的内容复制到这个临时的直接字节缓冲区执行底层的I/O操作。

直接缓冲区时I/O的最佳选择,可能创建直接缓冲区比创建非直接缓冲区的成本要高。直接缓冲区使用的内存是通过调用本地操作系统代码分配的,绕过了JVM的堆栈,现在JVM可能会执行缓冲区的缓存的优化.

ByteBuffer.allocateDirect()方法创建直接字节缓冲区


Channel

Channel是一种新的I/O访问方式,用于在字节缓冲区与通道另一侧的实体(可以是文件也可以是Socket)之间进行传输数据。

Channel可以双向读写数据,也开业实现异步读写。

程序不能直接访问Channel,Channel只能与Buffer缓冲区进行交互,即把通道中的数据读到Buffer缓冲区中,程序从缓冲区中读取数据;

在写操作时,程序把数据写入Buffer缓冲区中,再把缓冲区的数据写入到Channel中.

常用的Channel:

  • FileChannel:读写文件的通道
  • SocketChannel/ServerSocketChannel:读写Socket套接字中的数据
  • DatagramChannel:通过UDP读写网络数据。

注意:

1.通道不能通过构造方法创建

2.通道不能重复使用,一旦创建了一个通道,就表示一个特定的I/O服务建立了一个连接,一旦通道关闭,连接就没了

3.通道可以以默认阻塞的方式运行,也可设置为非阻塞方式运行


Scatter与Gather

Scatte(发散),Gather(聚焦)是通道提供的一个重要功能(有时也成为矢量I/O)

Scatte,Gather是指在多个缓冲区中实现一个简单的I/O操作。

Scatter是指从channel 通道中读取数据,把这些数据按顺序分散写入到多个Buffer缓冲区中

Gather是指在写操作时,将多个Buffer缓冲区的数据写入到同一个Channel中


Scatter,Gather经常用于需要将传输的数据分开处理的场景

如Scatter从一个Channel中读取数据存储到多个Buffer:

//示例伪代码
ByteBuffer c1=ByteBuffer.allocate(3);
ByteBuffer c2=ByteBuffer.allocate(3);
ByteBuffer[] bufArray=buf1Java中的NIO基础知识

JAVA NIO Selector 知识三

netty基础-NIO的selector

IOJava IO NIO 整理

Netty之NIO基础-Channel,Buffer,Selector

Java NIO 之 Selector 练习