问题来源在传统的架构中,对于客户端的每一次请求,服务器都会创建一个新的线程或者利用线程池复用去处理用户的一个请求,然后返回给用户结果,这样做在高并发的情况下会存在非常严重的性能问题:对于用户的每一次请求都创建一个新的线程是需要一定内存的,同时线程之间频繁的上下文切换也是一个很大的开销。
p.s: 本文涉及的完整实例代码都可以在我的GitHub上面下载。
什么是SelectorNIO的核心就是Selector,读懂了Selector就理解了异步机制的实现原理,下面先来简单的介绍一下什么是Selector。现在对于客户端的每一次请求到来时我们不再立即创建一个线程进行处理,相反以epool为例子当一个事件准备就绪之后通过回调机制将描述符加入到阻塞队列中,下面只需要通过遍历阻塞队列对相应的事件进行处理就行了,通过这种回调机制整个过程都不需要对于每一个请求都去创建一个线程去单独处理。上面的解释还是有些抽象,下面我会通过具体的代码实例来解释,在这之前我们先来了解一下NIO中两个基础概念Buffer和Channel。
如果大家对于多路IO复用比如select/epool完全陌生的话,建议先读一下我的这篇Linux下的五种IO模型 :-)
Buffer以ByteBuffer为例子,我们可以通过ByteBuffer.allocate(n)来分配n个字节的缓冲区,对于缓冲区有四个重要的属性:
- capacity,缓冲区的容量,也就是我们上面指定的n。
- position,当前指针指向的位置。
- mark,前一个位置,这里我们下面再解释。
- limit,最大能读取或者写入的位置。
如上图所示,Buffer实际上也是分为两种,一种用于写数据,一种用于读取数据。
put通过直接阅读ByteBuffer源码可以清晰看出put方法是把一个byte变量x放到缓冲区中去,同时position加1:
1
2
3
4
5
6
7
8
9
| public ByteBuffer put(byte x) {
hb[ix(nextPutIndex())] = x;
return this;
}
final int nextPutIndex() {
if (position >= limit)
throw new BufferOverflowException();
return position++;
}
|
getget方法是从缓冲区中读取一个字节,同时position加一:
1
2
3
4
5
6
7
8
| public byte get() {
return hb[ix(nextGetIndex())];
}
final int nextGetIndex() {
if (position >= limit)
throw new BufferUnderflowException();
return position++;
}
|
flip如果我们想将buffer从写数据的情况变成读数据的情况,可以直接使用flip方法:
1
2
3
4
5
6
| public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
|
mark和resetmark是记住当前的位置用的,也就是保存position的值:
1
2
3
4
| public final Buffer mark() {
mark = position;
return this;
}
|
如果我们在对缓冲区读写之前就调用了mark方法,那么以后当position位置变化之后,想回到之前的位置可以调用reset会将mark的值重新赋给position:
1
2
3
4
5
6
7
| public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
|
|