初探Java的Buffer类

传统阻塞型I/O的问题
用过Java的Socket编程的人一定都知道传统的网络I/O编程是ServerSocket的accept方法一直等待着TCP请求的接入,每当收到一个TCP请求后,ServerSocket就会创建出一组I/O流,把它们交给一个线程去处理,这种情况下的结构关系就是每条线程处理一个I/O,就像下面这张图一样
初探Java的Buffer类_第1张图片
这种设计有几个问题:
1.假设访问的高峰期并发量较大,我们必须为程序配置一个较大的线程池,可是当过了高峰期,并发量减少了,那些空闲的线程岂不是一种资源的浪费?
2.当I/O流打开以后如果数据传输并不频繁,那么这个线程很大一部分时间都是在等待中度过的,这又是一种资源的利用率低下。

那么怎么让更少的线程能够处理更多的事情呢?于是就引出了我们要讲的nio。
nio就是非阻塞型I/O(non-blocking I/O),它有三个核心的概念:Selector,Channel,Buffer。
它们的关系就像下图一样
初探Java的Buffer类_第2张图片
Buffer(缓冲区)的底层其实就是一个数组,它提供了一些方法来向数组中写入和读出数据。
Channel(通道)是一个可以向Buffer中写入和读取数据的对象,但是其本身不能直接读取数据。
这个博客主要是探索Buffer类的一些底层实现,channel类以后再去看。

Buffer的几个常用方法:

  • allocate() - 初始化一块缓冲区
  • put() - 向缓冲区写入数据
  • get() - 向缓冲区读数据
  • filp() - 将缓冲区的读写模式转换
  • clear() - 这个并不是把缓冲区里的数据清除,而是利用后来写入的数据来覆盖原来写入的数据,以达到类似清除了老的数据的效果
  • compact() - 从读数据切换到写模式,数据不会被清空,会将所有未读的数据copy到缓冲区头部,后续写数据不会覆盖,而是在这些数据之后写数据
  • mark() - 对position做出标记,配合reset使用
  • reset() - 将position置为标记值

Buffer的几个核心属性:

  • capacity - 缓冲区大小,缓冲区一旦创建出来以后,这个属性就不会再变化了
  • position - 读写数据的定位指针,用来标识当前读取到了哪一个位置。
  • limit - 读写的边界,用于限制指针的最大指向位置。当指针走到边界上的时候就要停住,否则就会抛出BufferUnderflowException

Buffer的基本用法
使用Buffer读写数据一般遵循以下四个步骤:

  1. 写入数据到Buffer
  2. 调用flip()方法改变读写模式
  3. 从Buffer中读取数据
  4. 调用clear()方法或者compact()方法
    下面用代码来展示下
public static void main(String[] args) {
        //生成一个长度为10的缓冲区
        IntBuffer intBuffer = IntBuffer.allocate(10);
        for (int i = 0; i < intBuffer.capacity(); ++i){
            int randomNum = new SecureRandom().nextInt(20);
            intBuffer.put(randomNum);
        }
        //状态翻转
        intBuffer.flip();
        while (intBuffer.hasRemaining()){
            //读取数据
            System.out.print(intBuffer.get() + ",");
        }
        //clear方法本质上并不是删除数据
        intBuffer.clear();
        System.out.print("\n");
        System.out.println("-----------------------------");
        while (intBuffer.hasRemaining()){
            System.out.print(intBuffer.get() + ",");
        }
    }

控制台输出如下
初探Java的Buffer类_第3张图片
可以看到,调用了clear方法以后,缓冲区里的数据并没有被清除。这是什么原因呢,先不急,先改写一下上面的代码。

public static void main(String[] args) {
        IntBuffer intBuffer = IntBuffer.allocate(10);
        System.out.println("初始的Buffer:" + intBuffer);
        for (int i = 0; i < 5; ++i){
            int randomNum = new SecureRandom().nextInt(20);
            intBuffer.put(randomNum);
        }

        System.out.println("flip之前:limit = "+ intBuffer);
        intBuffer.flip();
        System.out.println("flip之后:limit = "+ intBuffer);

        System.out.println("进入读取");
        while (intBuffer.hasRemaining()){
            System.out.println(intBuffer);
            System.out.println(intBuffer.get());
        }
    }

控制台输出结果如下:
初探Java的Buffer类_第4张图片
输出结果中的pos代表的就是position属性,lim代表limit属性,cap代表capacity属性。
在缓冲区刚初始化出来的时候,position指向的是数组的第一个位置,limit和数组的容量一样。
用图片来表示大概就如下图
初探Java的Buffer类_第5张图片
向缓冲区中每写入一个数据,指针就会后移一位
初探Java的Buffer类_第6张图片
当调用了flip()方法后,position又会重新指向数组的第一个位置,而limit会指向原来的position的位置。
初探Java的Buffer类_第7张图片
从源码上来看,就是把position赋值给limit,再把position变回0。
如果用图示的话就如下图:
初探Java的Buffer类_第8张图片
然后再调用get方法的时候,每调用读取一个数据,position就会向后移动一位,直到position到达了limit的位置。实际上intBuffer.hasRemaining()方法就是判断position < limit。

你可能感兴趣的:(Java语言基础)