传统阻塞型I/O的问题
用过Java的Socket编程的人一定都知道传统的网络I/O编程是ServerSocket的accept方法一直等待着TCP请求的接入,每当收到一个TCP请求后,ServerSocket就会创建出一组I/O流,把它们交给一个线程去处理,这种情况下的结构关系就是每条线程处理一个I/O,就像下面这张图一样
这种设计有几个问题:
1.假设访问的高峰期并发量较大,我们必须为程序配置一个较大的线程池,可是当过了高峰期,并发量减少了,那些空闲的线程岂不是一种资源的浪费?
2.当I/O流打开以后如果数据传输并不频繁,那么这个线程很大一部分时间都是在等待中度过的,这又是一种资源的利用率低下。
那么怎么让更少的线程能够处理更多的事情呢?于是就引出了我们要讲的nio。
nio就是非阻塞型I/O(non-blocking I/O),它有三个核心的概念:Selector,Channel,Buffer。
它们的关系就像下图一样
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读写数据一般遵循以下四个步骤:
写入数据到Buffer
调用flip()方法改变读写模式
从Buffer中读取数据
调用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() + ",");
}
}
控制台输出如下
可以看到,调用了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());
}
}
控制台输出结果如下:
输出结果中的pos代表的就是position属性,lim代表limit属性,cap代表capacity属性。
在缓冲区刚初始化出来的时候,position指向的是数组的第一个位置,limit和数组的容量一样。
用图片来表示大概就如下图
向缓冲区中每写入一个数据,指针就会后移一位
当调用了flip()方法后,position又会重新指向数组的第一个位置,而limit会指向原来的position的位置。
从源码上来看,就是把position赋值给limit,再把position变回0。
如果用图示的话就如下图:
然后再调用get方法的时候,每调用读取一个数据,position就会向后移动一位,直到position到达了limit的位置。实际上intBuffer.hasRemaining()方法就是判断position < limit。