三大核心原理示意图
说明:
- 每个
Channel
都会对应一个Buffer
;Selector
对应一个线程,一个线程对应多个Channel
(连接);- 该图反应了有三个
Channel
注册到该Selector
//程序 ;- 程序切换到哪个
Channel
是由事件决定的,Event
就是一个重要的概念;Selector
会根据不同的事件,在各个通道上切换;Buffer
就是一个内存块,底层是有一个数组;- 数据的读取写入是通过
Buffer
,BIO
中要么是输入流,或者是输出流,不能双向,但是NIO
的Buffer
是可以读也可以写,需要flip
方法切换。Channel
是双向的,可以返回底层操作系统的情况,比如Linux
,底层的操作系统通道就是双向的。
一、Buffer分类
定义:缓冲区Buffer在java nio中负责数据的存储,缓冲区Buffer本质就是数组,用于存储不同类型数据的数组,nio提供了一整套读写API。
jdk为java七大基本类型数据都准备了响应的缓冲区(boolean值除外):
上述缓冲区除了ByteBuffer的 功能稍微多点外,因为ByteBuffer是通用的,所以功能会比较多。其他6种的使用方式几乎是一致的。都是通过如下方法获取一个 Buffer对象:
static XxxBuffer allocate(int capacity) : 创建一个容量为capacity 的 XxxBuffer 对象
二、缓冲区的基本属性
Buffer 中的重要概念:
- 容量 (capacity) :表示 Buffer 最大数据容量,缓冲区容量不能为负,并且创建后不能更改。
- 限制 (limit) :第一个不应该读取的数据的索引,即位于 limit 后的数据不可读。缓冲区的限制不能为负,并且不能大于其容量。
- 位置 (position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制
- 标记 (mark) 与重置 (reset) :标记是一个索引,通过 Buffer 中的 mark() 方法指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这个 position.
标记 、 位置 、 限制 、 容量遵守以下不变式: 0 <= mark <= position <= limit <= capacity
缓冲区的基本属性:
Buffer有分读模式和写模式,其实质是由limit值和position值决定的。这种模式没有特定的死规定。
Buffer 的常用方法:
缓冲区的数据操作:
Buffer 所有子类提供了两个用于数据操作的方法:get() 与 put() 方法
获取 Buffer 中的数据
- get() :读取单个字节
- get(byte[] dst):批量读取多个字节到 dst 中
- get(int index):读取指定索引位置的字节(不会移动 position)
放入数据到 Buffer 中
- put(byte b):将给定单个字节写入缓冲区的当前位置
- put(byte[] src):将 src 中的字节写入缓冲区的当前位置
- put(int index, byte b):将指定字节写入缓冲区的索引位置(不会移动 position)
三、Buffer源码分析
/*
* Buffer以上七大基本类型缓冲区类的基类,是个抽象类,定义了一些通用方法以及一些抽象方法。
* 具体方法都是final修饰的,不可被子类覆盖。
*/
public abstract class Buffer {
// 在缓冲区中维护遍历和分割元素的拆分特征??
static final int SPLITERATOR_CHARACTERISTICS =
Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED;
//标记当前的position 位置,用reset方法的话,可以把position设置为mark值位置。
//如果mark=-1的话会reset失败。
private int mark = -1;
//游标,标记缓冲区下一个要读取或者写入的数组下标。
private int position = 0;
//表示缓冲区数组可以操作(读取、写入)的可用数据长度。
private int limit;
//缓冲区容量
private int capacity;
//以上四个属性符合 mark<=position<=limit<=capacity,不符合就会报错。
// 只对直接缓冲区有用
// 记录直接缓冲区的内存地址,用于提高JNI 的方法GetDirectBufferAddress的效率。
//ByteBuffer才支持直接缓冲区,所以到后面关于ByteBuffer的会讲。
long address;
// 传入mark、position、limit、capacity值创建一个缓冲区的构造器
//是个包内私有的构造器
Buffer(int mark, int pos, int lim, int cap) {
//缓冲区容量小于0时抛出异常。
if (cap < 0)
throw new IllegalArgumentException("Negative capacity: " + cap);
//根据传参设置缓冲区大小。
this.capacity = cap;
//设置limit
limit(lim);
//设置position
position(pos);
if (mark >= 0) {
if (mark > pos)
//mark大于pos并且mark大于等于0时会抛出异常
throw new IllegalArgumentException("mark > position: ("
+ mark + " > " + pos + ")");
//设置mark
this.mark = mark;
}
}
/**
* 返回缓冲区容量
*/
public final int capacity() {
return capacity;
}
/**
* 返回缓冲区下一次要读取或者写入的数组下标
*/
public final int position() {
return position;
}
/**
* 设置缓冲区的游标
*/
public final Buffer position(int newPosition) {
//要设置的position值不能大于limit值或者小于0,否则就抛出异常。
if ((newPosition > limit) || (newPosition < 0))
throw new IllegalArgumentException();
//设置游标
position = newPosition;
//如果设置的新的游标值比mark值小,就重置mark值为-1.
if (mark > position) mark = -1;
return this;
}
/**
*返回缓冲区的可用数据长度。
*/
public final int limit() {
return limit;
}
/**
* 设置缓冲区的limit值,同时会保证position和mark值保证这几个值的关系符合规则。
*/
public final Buffer limit(int newLimit) {
//对要设置的新limit进行检查,如果大于缓冲区容量或者小于0就抛出异常
if ((newLimit > capacity) || (newLimit < 0))
throw new IllegalArgumentException();
//设置limit值
limit = newLimit;
//如果游标比可操作的最大数组下标还大的话,就把游标设置为limit
if (position > limit) position = limit;
//如果mark > limit,重置mark。
if (mark > limit) mark = -1;
return this;
}
/**
* 标记当前position位置。
*/
public final Buffer mark() {
mark = position;
return this;
}
/**
* 把游标恢复到标记的位置
*/
public final Buffer reset() {
int m = mark;
//标记小于0时抛出异常
if (m < 0)
throw new InvalidMarkException();
//重置游标至标记处
position = m;
return this;
}
/**
* 清空缓冲区,其实就设置游标为0,limit设置回capacity值,mark重置,数据还是存在的。
*/
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
/**
* 缓冲区创建时默认是写模式的,这个方法把缓冲区改为读模式。
* 每次通过通道往存储设备中写数据都需要调用此方法把缓冲区设置为读模式。读取缓冲区的数据。
* 最后详细讲解这个
*/
public final Buffer flip() {
//把limit设置为position
limit = position;
//将游标设置为0
position = 0;
//重置mark。
mark = -1;
return this;
}
/**
* 重置游标,从新开始读、写数据。
*/
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
/**
* 读模式下,返回剩余可读的数据长度,写模式下,返回剩余可写的缓冲区长度。
*/
public final int remaining() {
return limit - position;
}
/**
* 返回是否还有数据可读或者可写。
*/
public final boolean hasRemaining() {
return position < limit;
}
/**
* 返回缓冲区是否只读,抽象方法,具体看实现类。
*/
public abstract boolean isReadOnly();
/**
* 是否直接缓冲区,true为直接缓冲区
*/
public abstract boolean isDirect();
//分割线,上面是公共方法API,下面是包私有方法,我们不能调用的//
/**
* 读模式下游标往右移一位,也就是跳过一个数据。
* 返回移动前的游标值。
*/
final int nextGetIndex() { // package-private
//因为游标加一 所以要确保游标加一前要小于等于limit
if (position >= limit)
//当前游标>=limit抛出异常
throw new BufferUnderflowException();
return position++;
}
/**
* 读模式下游标往右移动n位。
* 返回移动前的游标
*/
final int nextGetIndex(int nb) { // package-private
if (limit - position < nb)
//判断加n后的游标是否大于limit,大于就抛出异常
throw new BufferUnderflowException();
int p = position;
position += nb;
//返回移动前的游标。
return p;
}
/**
* 写模式下游标往右移动1位。
* 返回移动前的游标
* 逻辑与上面的一样。
*/
final int nextPutIndex() { // package-private
if (position >= limit)
throw new BufferOverflowException();
return position++;
}
/**
* 写模式下游标往右移动n位。
* 返回移动前的游标
* 逻辑与上面的一样。
*
final int nextPutIndex(int nb) { // package-private
if (limit - position < nb)
throw new BufferOverflowException();
int p = position;
position += nb;
return p;
}
final int markValue() { // package-private
//返回标记的值
return mark;
}
//把缓冲区置为不可用
final void truncate() { // package-private
//重置mark为-1
mark = -1;
//啥都置零
position = 0;
limit = 0;
capacity = 0;
}
final void discardMark() { // package-private
//重置游标-1
mark = -1;
}
}