传统的BIO对于每一个新的网络连接,都通过线程池分配给一个专门线程去负责IO处理。每个线程都独自处理自己负责的socket连接的输入和输出。当然,服务器的监听线程也是独立的,任何的socket连接的输入和输出处理,不会阻塞到后面新socket连接的监听和建立,这样,服务器的吞吐量就得到了提升。【早期版本的Tomcat服务器,就是这样实现的。】
传统的BIO模型在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的I/O并且编程模型简单,也不用过多考虑系统的过载、限流等问题。此模型往往会结合线程池使用,线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。比如以下示例:
static ExecutorService executorService = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = null;
while(true){
socket = serverSocket.accept();
handle(socket);
}
}
private static void handle(Socket socket) {
executorService.execute(new Runnable() {
@Override
public void run() {
try {
byte[] bytes = new byte[1024];
socket.getInputStream().read(bytes);
System.out.println(new String(bytes));
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
但是模型最本质的问题在于,严重依赖于线程。但线程是很”贵”的资源,主要表现在:
注:CPU 利用率为 CPU 在用户进程、内核、中断处理、IO 等待以及空闲时间五个部分使用百
分比。人们往往通过五个部分的各种组合,用来分析 CPU 消耗情况的关键指标。CPU sy 值表
示内核线程处理所占的百分比。
使用linux 的top命令去查看当前系统的资源,会输出下面的一些指标:
因此当CPU sy 值高时,表示系统调用耗费了较多的 CPU,对于 Java 应用程序而言,造成这种现象的主要原因是启动的线程比较多,并且这些线程多数都处于不断的等待(例如锁等待态)和执行状态的变化过程中,这就导致了操作系统要不断的调度这些线程,切换执行。
4.容易造成锯齿状的系统负载。因为系统负载(System Load)是用活动线程数和等待线程数来综合计算的,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求同时到来,从而激活大量阻塞线程从而使系统负载压力过大。
注:系统负载(System Load),指当前正在被 CPU 执行和等待被 CPU 执行的进程数目总和,是反映系统忙闲程度的重要指标。当 load 值低于 CPU 数目时,表示 CPU 有空闲,资源存在浪费;当 load 值高于 CPU 数目时,表示进程在排队等待 CPU,表示系统资源不足,影响应用程序的执行性能。
因此,当面对十万甚至百万级连接的时候,传统的BIO模型是无能为力的。
但是,高并发的需求却越来越普通,随着移动端应用的兴起和各种网络游戏的盛行,百万级长连接日趋普遍,此时,必然需要一种更高效的I/O处理组件——这就是Java 的NIO编程组件。
在1.4版本之前,Java IO类库是阻塞式IO;从1.4版本开始,引进了新的异步IO库,被称为Java New IO类库,简称为Java NIO。
Java NIO类库的目标,就是要让Java支持非阻塞IO,基于这个原因,更多的人喜欢称Java NIO为非阻塞IO(Non-Block IO),称“老的”阻塞式Java IO为OIO(Old IO)。总体上说,NIO弥补了原来面向流的OIO同步阻塞的不足,它为标准Java代码提供了高速的、面向缓冲区的IO。
Java NIO类库包含以下三个核心组件:
在Java中,NIO和OIO的区别,主要体现在三个方面:
1、OIO是面向流(Stream Oriented)的,NIO是面向缓冲区(Buffer Oriented)的。
2、OIO的操作是阻塞的,而NIO的操作是非阻塞的。
3、OIO没有选择器(Selector)概念,而NIO有选择器的概念。
NIO技术的实现,是基于底层的IO多路复用技术实现的,比如在Windows中需要select多路复用组件的支持,在Linux系统中需要select/poll/epoll多路复用组件的支持。所以NIO需要底层操作系统提供支持。而OIO不需要用到选择器。
在OIO中,同一个网络连接会关联到两个流:一个输入流(Input Stream),另一个输出流(Output Stream),Java应用程序通过这两个流,不断地进行输入和输出的操作。
在NIO中,一个网络连接使用一个Channel(通道)表示,所有的NIO的IO操作都是通过连接通道完成的。一个通道类似于OIO中的两个流的结合体,既可以从通道读取数据,也可以向通道写入数据。
Channel和Stream的一个显著的不同是:Stream是单向的,譬如InputStream是单向的只读流,OutputStream是单向的只写流;而Channel是双向的,既可以用来进行读操作,又可以用来进行写操作。
NIO中的Channel的主要实现有:
要Channel的本质,还得回到TCP/IP协议的四层模型的基础知识。具体如下图所示。
在TCP/IP协议四层模型的最底层为链路层。在最原始的物理链路时代,咱们数据传输的两头(发送方和接收方)会通过拉同轴电缆的方式,拉一条物理电缆(类似于后来更加高级的网线),这条网线就代表一个双向的连接(connection),通过这条电缆,双方可以完成数据的传输。数据传输一旦完成,需要把这条物理链路拆除(就是这么粗暴)。
而在操作系统的维度,该怎么标识这种底层的物理链路的,下面我们来看看:
操作系统一切都是文件描述符(file descriptor)。所以,这种底层的物理链路,在操作系统层面,就会为应用创建一个文件描述符(file descriptor)。
这点和Java里边的对象类似,一个Java对象有内存的数据结构和内存地址,那么,一个文件描述符(file descriptor)也有一个内核的数据结构和一个进程内的唯一编号来表示。然后,操作系统会把这个文件描述提供给应用层,应用层通过对这个文件描述符(file descriptor)去对传输链路进行数据的读取和写入。
注:这里要把文件描述符和文件两个概念,稍加区分。文件这个概念,狭义的理解,就是磁盘上的文件。实际上,Linux 上的文件描述符,除了对磁盘文件做引用之外,还可以引用非磁盘文件。
NIO中的TCP传输通道,实际上就是对底层的传输链路所对应的文件描述符(file descriptor)的一种封装,具体的代码如下:
class SocketChannelImpl extends SocketChannel implements SelChImpl {
private static NativeDispatcher nd;
private final FileDescriptor fd;//文件描述符
.....
}
public final class FileDescriptor {
private int fd;//文件描述符 的 进程内的唯一编号
private Closeable parent;
private List otherParents;
private boolean closed;
.....
}
如果两个Java应用通过NIO建立双向的连接(传输链路),它们各自都会有一个自己内部的文件描述符(file descriptor),代表这条连接的自己一方,如下图所示:
1、IO多路复用模型
IO多路复用指的是一个进程/线程可以同时监视多个文件描述符(含socket连接),一旦其中的一个或者多个文件描述符可读或者可写,该监听进程/线程能够进行IO事件的查询。
2、监视对多个文件描述符
Java NIO组件【Selector 选择器】是一个IO事件的监听与查询器。通过选择器,一个线程可以查询多个通道的IO事件的就绪状态。
3、IO事件
IO事件表示通道某种IO操作已经就绪、或者说已经做好了准备。例如,如果一个新Channel链接建立成功了,就会在Server Socket Channel上发生一个IO事件,代表一个新连接一个准备好,这个IO事件叫做“接收就绪”事件。再例如,一个Channel通道如果有数据可读,就会发生一个IO事件,代表该连接数据已经准备好,这个IO事件叫做 “读就绪”事件。
Java NIO将NIO事件进行了简化,只定义了四个事件,这四种事件用SelectionKey的四个常量来表示:
Selector的本质,就是去查询这些IO就绪事件,所以,它的名称就叫做Selector查询者。
从编程实现维度来说,IO多路复用编程的第一步,是把通道注册到选择器中,第二步则是通过选择器所提供的事件查询(select)方法,这些注册的通道是否有已经就绪的IO事件(例如可读、可写、网络连接完成等)。
由于一个选择器只需要一个线程进行监控,所以,我们可以很简单地使用一个线程,通过选择器去管理多个连接通道。
与OIO相比,NIO使用选择器的最大优势:系统开销小,系统不必为每一个网络连接(文件描述符)创建进程/线程,从而大大减小了系统的开销。
因此,通过Java NIO可以达到一个线程负责多个连接通道的IO处理,这是非常高效的。这种高效,恰恰就来自于Java的选择器组件Selector以及其底层的操作系统IO多路复用技术的支持。
应用程序与通道(Channel)主要的交互,主要是进行数据的read读取和write写入。为了完成NIO的非阻塞读写操作,NIO为大家准备了第三个重要的组件——NIO Buffer(NIO缓冲区)。
Buffer缓冲区,实际上是一个容器,一个连续数组。Channel提供从文件、网络读取数据的渠道,但是读写的数据都必须经过Buffer。
通道的读取,就是将数据从通道读取到缓冲区中;
通道的写入,就是将数据从缓冲区中写入到通道中 ;
缓冲区的使用,是面向流进行读写操作的OIO所没有的,也是NIO非阻塞的重要前提和基础之一。