在进入主题之前先看个 Java 网络编程的一个简单例子:代码很简单,客户端和服务端进行通信,对于客户端的每次输入,服务端回复 get。注意,服务端可以同时允许多个客户端连接。

服务端端代码:

// 创建服务端 socket
ServerSocket serverSocket = new ServerSocket(20000);
client = serverSocket.accept();

// 客户端连接成功,输出提示
System.out.println("客户端连接成功");
// 启动一个新的线程处理客户端请求
new Thread(new ServerThread(client)).start();

// 子线程中处理客户端的输入
class ServerThread implements Runnable {
.....
@Override
public void run() {
boolean flag = true;
while (flag) {

// 读取客户端发送来的数据
String str = buf.readLine();

// 回复给客户端 get 表示收到数据
out.println("get");
}
}
}

客户端代码 :

Socket client = new Socket("127.0.0.1", 20000);
boolean flag = true;
while (flag) {

// 读取用户从键盘的输入
String str = input.readLine();
// 把用户的输入发送给服务端
out.println(str);

// 接受到服务端回传的 get 字符串
String echo = buf.readLine();
System.out.println(echo);
}
}

考虑到完整的 Java 示例代码太过庞大影响阅读,所以这里不完整贴出,如果需要在 github 直接下载,这里是下载地址。

可以看到,server 为了能够同时处理多个 client 的请求,需要为每个 client 开启一个 thread,这种 one-thread-per-client 的模式对于 server 而言压力是很大的。假设有 1k 个 client,对应的 server 应该启动 1k 个 thread,那么 server 所耗费的内存,以及 thread 切换时候占用的时间等等都是致命伤。即使使用线程池的技术来限制线程个数,这种 blocking-IO 的模型还是没办法支撑大量连接。

每个 client 都需要一个 thread 来请求处理。

NIO

上面这种 one-thread-per-client 的模式无法支撑大量连接的主要原因在于readLine会阻塞 IO,即在readLine没能够读取到数据的时候,会一直阻塞线程,使得线程无法继续执行,那么 server 为了可以同时处理多个 client,只能同时开启多个线程。

所以,Java 1.4 之后引入了一套 NIO 接口。NIO 中最主要的一个功能就是可以进行非阻塞 IO 操作:如果没能够读取到数据,非阻塞 IO 不会阻塞线程,而是直接返回 0。这种情况下,线程通过返回值判断数据还没有准备好,就可以处理其他事情,而不会被阻塞。

上图是阻塞 IO 和非阻塞 IO 的区别,可以看出虽然 非阻塞 IO 并不会被阻塞,但是它仍然不断的调用函数检查数据是否已经可读,这种现象在代码中是以这种形式展现:

while((str = read()) == 0) {

}
// 继续读取到数据之后的逻辑。

可以明白,虽然非阻塞 IO 不会阻塞线程,但是由于没有数据可读,线程也没有办法继续执行下面的逻辑,只能不断的调用判断,等待数据到来。这种情况下称为同步 IO。所以综上,NIO 本质上是一个非阻塞同步 IO。

IO 复用

由于 NIO 不会因为数据还没有到达而被阻塞,那么就没有必要每一个 client 都分配一个 thread 不断去轮询判断是否有数据可读。可以使用一个 thread 监听所有的 client 连接,由这个 thread 循环判断是否有某个 client 的数据可读,如果有就告知其他 thread 某个 client 连接由数据可读。这种行为就被称之为IO 复用。 在 NIO 中提供了Selector类来监听所有 client 连接是否有数据可读。

使用Selector来实现 IO 复用,只有一个 thread 需要关心数据是否到来,其他线程等待通知就好。如此一来,只有监听线程会一直循环判断,并不会占据太多 CPU 资源。提到 NIO 中的Selector,不得不说一下 Linux 编程中的 IO 复用,因为 NIO 中的Selector底层就是使用系统级的 IO 复用方案。

Linux 系统的 IO 复用实现方案有 2 种:

  • select
  • epoll

在 Linux 2.6+ 的版本上 NIO 底层使用的是epoll,在 2.4.x 的版本使用的是select函数。epoll函数在性能方面比select好很多,这里可以不关心 Linux 编程具体细节。值得一提的是,Java 的 netty 网络框架底层就是使用 NIO 技术。

AIO

回顾一下 NIO 中:使用监听线程调用select函数来监听所有请求是否有数据到达,如果有数据则通知其他线程来读取数据。这里在线程读取数据的过程中,线程在数据没有读取完毕之前是处于阻塞状态,只有数据读取完毕之后线程才可以继续执行逻辑。之前说过,这种称之为同步 IO。JDK 7 中新增了一套新接口 AIO(Asynchronous IO)。

AIO 有一个神奇的特性:当发起 IO 操作之后,线程不用等待 IO 读取完毕,而是可以直接返回,继续执行其他操作。等到数据读取完毕之后,系统会通知线程数据已经读取完毕。这种发起 IO 操作,但是不必等待数据读取完毕的 IO 操作称之为异步 IO。如果使用 AIO,一个线程可以同时发起多个 IO 操作,这就意味着,一个线程可以同时处理多个请求。著名的 web 服务器 Nginx 就是用了异步 IO。关于更多的细节,可以参考下我的另一篇文章 <Apache--MPMs && Nginx事件驱动>。

End

到目前为止,文章解释了阻塞/非阻塞 IO,同步/异步 IO 的区别,谈起 IO 模型,不可避免会涉及 Linux 的 5 种 IO 模型

  • 阻塞 IO
  • 非阻塞 IO
  • IO 复用
  • 信号驱动 IO
  • 异步 IO

除去信号驱动 IO没有提及,其他 4 种主要的 IO 模型都有所解释,理解了这些 IO 模型的概念对于编写代码有很大的帮助。

Java学习交流QQ群:589809992 禁止闲聊,非喜勿进!

更多相关文章

  1. 重新认识Java线程的概念
  2. Java 简单解决springmvc获取properties文件里面中文内容出现论码
  3. JNDI学习总结(一)——JNDI数据源的配置
  4. java配置使用数据源
  5. java线程实现与进程(二)
  6. java多线程jdk1.7与jdk1.6结果不一致的问题
  7. Java线程的生命周期和状态控制
  8. “树”不倒,人不散—数据结构的核心
  9. 使用线程设置后台进程以处理Android中的工作

随机推荐

  1. Linux系统编程——进程间通信:信号中断处
  2. Linux下安装mysql 出现依赖包冲突解决方
  3. ubi文件系统制作,还是"-c"选项的问题
  4. 求GridControl_11.1.0.1.0_Linux_x86-64_
  5. Linux 系统下 DNS 服务器的架设
  6. Linux CentOS6环境下MySQL5.1升级至MySQL
  7. Linux 开/关 ICMP 回应(防止被ping)
  8. 《Unix & Linux 大学教程》 - 第十九章(一
  9. linux vim下如何让类似txt的文本,呈现出某
  10. 我从网上下载了RedHat Linux 9.03,是三个