原文链接:如何解读 Java IO、NIO 中的同步阻塞与同步非阻塞?

一、前言

最近刚读完一本书:《Netty、Zookeeper、Redis 并发实战》,个人觉得 Netty 部分是写得很不错的,读完之后又对 Netty 进行了一波很好的复习(之前用 spring boot + netty + zookeeper 模仿 dubbo 做 rpc 框架,那时候是刚学 netty 后自己造的小轮子)。

虽然对于 Netty 的使用已经比较熟悉了,而且还知道它的底层是基于 Java NIO 做进一步的封装,使得并发性能和开发效率得到大大的提升。但是,对于同步阻塞、同步非阻塞、异步这些概念,还是比较的模糊,一直处于似懂非懂的状态。

所以这两天,一直在网上看看大家对此的评论,也得到了一些启发。而且还有很多同学们提到了 《Netty 权威指南 第二版》 这本书,说前两章对于网络 I/O 模型和 Java I/O 的介绍很不错,所以我也特意去找了一本 pdf 来看看(比较穷。。。)。看了前两章后,确实对于这方面的概念清晰了不少,所以决定写下此文章来记录一下,也分享给更多不清楚这方面理论的同学们,并且也下定决定,有空一定把这本书继续看完,哈哈哈。

二、Linux 网络 I/O 模型

其实我们一直说到的同步异步、阻塞非阻塞,都是基于系统内核提供的系统命令来说的;而我们通常都是使用 Linux 系统的服务器,所以我们很有必要去了解关于 Linux 系统内核的相关概念,而最重要的是,UNIX 网络编程对 I/O 模型的分类。

UNIX 提供了五种 I/O 模型:

  1. 阻塞 I/O 模型:缺省情况下,所有文件操作都是阻塞的。我们以套接字接口为例讲解此模型:在进程空间中调用 recvfrom,其系统调用知道数据包到达且被复制到应用进程的缓冲区中或者发生错误时才返回,在此期间一直会等待,进程在从调用 recvfrom 开始到它返回的整段时间内都是被阻塞的,因此被称为阻塞 I/O 模型。

  2. 非阻塞 I/O 模型:recvfrom 从应用层到内核的时候,如果该缓冲区没有数据的话,就直接返回一个 EWOULDBLOCK 错误,一般都对非阻塞 I/O 模型进行轮询检查这个状态,看内核是不是有数据到来。

  3. I/O 复用模型:Linux 提供 select/poll 进程通过将一个或多个 fd 传递给 select 或 poll 系统调用,阻塞在 select 操作上,这样 select/poll 可以帮我们侦测多个 fd 是否处于就绪状态。select/poll 是顺序扫描 fd 是否就绪,而且支持的 fd 数量有限,因此它的使用收到了一下制约。Linux 还提供了一个 epoll 系统调用,epoll 使用基于事件驱动方式代替顺序扫描,因此性能更高。当有 fd 就绪时,立刻回调函数 rollback。

  4. 信号驱动 I/O 模型:首先开启套接口信号驱动 I/O 功能,并通过系统调用 sigaction 执行一个信号处理函数(此系统调用立刻返回,进程继续工作,它是非阻塞的)。当数据准备就绪时,就为该进程生成一个 SIGIO 信号,通过信号回调通知应用程序调用 recvfrom 来读取数据,并通知主循环函数处理数据。

  5. 异步 I/O:告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核复制到用户自己的缓冲区)通知我们。这种模型与信号驱动模型的主要区别是:信号驱动 I/O 由内核通知我们何时可以开始一个 I/O 操作;而异步 I/O 模型由内核通知我们 I/O 操作何时已经完成。

以上资料摘自《Netty 权威指南 第2版》。

三、Java 中 IO 和 NIO

我们都知道 Java 中:IO 是同步阻塞,而 NIO 是同步非阻塞;而经过上面关于 Liunx 网络 I/O 模型的解读,我们都已经比较清楚地了解了同步异步和阻塞非阻塞的概念。那么我们接下来应该从编程中去解读 Java IO 的同步阻塞和 Java NIO 的同步非阻塞。

Java IO 编程:

1、我们先看看 Java IO 编程中的服务端代码:

public class IOServer {
public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket(8000); // (1) 接收新连接线程
new Thread(() -> {
while (true) {
try {
// (1) 阻塞方法获取新的连接
Socket socket = serverSocket.accept(); // (2) 每一个新的连接都创建一个线程,负责读取数据
new Thread(() -> {
try {
int len;
byte[] data = new byte[2];
InputStream inputStream = socket.getInputStream();
// (3) 按字节流方式读取数据
while ((len = inputStream.read(data)) != -1) {
System.out.println(new String(data, 0, len));
}
} catch (IOException e) {
}
}).start(); } catch (IOException e) {
} }
}).start();
}
}

在 IOServer 中,会开着 while 死循环一直调用 ServerSocket#accpet() 方法来监听等待客户端连接:ServerSocket 主动监听是否有客户端请求连接,如果没有的话就会一直阻塞等待着,所以说 IO 是同步阻塞的;

当 ServerSocket 接收到新的连接请求,一般会创建一条新线程来处理接下来客户端的写请求(当然了,也可以在同一条线程中处理);在线程里面,会调用 Socket 输入流(InputStream)的 read(byte b[]) 方法来读取客户端发送过来的数据:该方法会一直阻塞着,直到客户端发送数据过来;当发现内核态中有数据了,就会将数据复制到用户态中(也就是字节数组中),所以说 IO 是同步阻塞的。

弊端:当消息发送方发送请求比较缓慢,或者网络传输比较慢时,消息接收方的读取输入流会被长时间堵塞,直到发送方的数据发送完成。

2、接下来继续看看 Java IO 编程中的客户端代码:

public class IOClient {

    public static void main(String[] args) {
new Thread(() -> {
try {
Socket socket = new Socket("127.0.0.1", 8000);
while (true) {
try {
socket.getOutputStream().write((new Date() + ": hello world").getBytes());
Thread.sleep(2000);
} catch (Exception e) {
}
}
} catch (IOException e) {
}
}).start();
}
}

在 IOClient 中,开着 while 死循环一直调用客户端 Socket 输出流(OutputStream)的 write(byte b[]) 方法往服务端发送数据;而此时,客户端会一直阻塞着,直到所有的字节全部写入完毕或者发生异常。

弊端:当消息接收方处理比较缓慢时,最后可能会导致 TCP 的缓冲区充满未被处理的数据;此时消息发送方不能再继续往 TCP 缓冲区写入消息,会一直被阻塞着。

3、Java IO 同步阻塞解读:

在 Java IO 中,不管是服务端还是客户端,不管是读取数据还是写入数据,都需要自己主动去完成这个 I/O 操作,这就是同步。而如果对方处理消息的效率比较慢,进程可能会因为执行此次 I/O 操作而导致被一直阻塞着,这就是阻塞。

Java NIO 编程:

1、我们先看看 Java NIO 编程中的服务端代码:

public class NIOServer {
public static void main(String[] args) throws IOException {
Selector serverSelector = Selector.open();
Selector clientSelector = Selector.open(); new Thread(() -> {
try {
// 对应IO编程中服务端启动
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
listenerChannel.socket().bind(new InetSocketAddress(8000));
listenerChannel.configureBlocking(false);
listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT); while (true) {
// 监测是否有新的连接,这里的1指的是阻塞的时间为 1ms
if (serverSelector.select(1) > 0) {
Set<SelectionKey> set = serverSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator(); while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next(); if (key.isAcceptable()) {
try {
// (1) 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
clientChannel.register(clientSelector, SelectionKey.OP_READ);
} finally {
keyIterator.remove();
}
} }
}
}
} catch (IOException ignored) {
} }).start(); new Thread(() -> {
try {
while (true) {
// (2) 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为 1ms
if (clientSelector.select(1) > 0) {
Set<SelectionKey> set = clientSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator(); while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next(); if (key.isReadable()) {
try {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// (3) 面向 Buffer
clientChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
.toString());
} finally {
keyIterator.remove();
key.interestOps(SelectionKey.OP_READ);
}
} }
}
}
} catch (IOException ignored) {
}
}).start(); }
}

在 NIOServer 中,会创建并打开两个 Selector ,Selecotr 是 Java NIO 中的核心组件,底层利用的是 I/O 多路复用模型。

  • 一个 Selector 负责监听 ServerSocketChannel 中的客户端连接请求,如果有新的客户端请求连接,那么就会创建对应的 SocketChannel,然后往另外一个 Selector 中注册;如果没有,则直接返回,不会在这里阻塞着,进程可以继续做别的事情,所以 NIO 是同步非阻塞。

  • 第二个 Selecotr,就是负责监听哪些 SocketChannel 有读写事件,如果有的话则进行对应的 I/O 操作;而如果没有,也是直接返回,不会在这里一直阻塞着,进程可以继续做别的事情,所以 NIO 是同步非阻塞。

2、Java NIO 中客户端的编程:

这个我们就不用上代码了,其实和服务端中第二个 Selector 的使用一样的。

3、Java NIO 同步非阻塞解读:

在 Java NIO 中,不管是服务端还是客户端,都会将自己注册到 Selector 中,如果哪个 Channel 有请求连接事件( ServerSocketChannel)或者是读写事件(SocketChannel),那么这个 Channel

就会处于就绪状态;接着会被 Selector 轮询出来,进行后续的 I/O 操作。这就不会出现 IO 编程中的阻塞状态,所以 NIO 是同步非阻塞的。

四、总结

通过上面的讲解分析,可能还是会有很多同学不能真正理解同步异步、阻塞非阻塞这些概念,毕竟这些是我自己个人的理解和解读,所以我还是非常推荐同学们自己去看看《Netty 权威指南》这本书,和看看 Java 中关于 IO 和 NIO 编程的相关源码,一定要让自己理解地更加深刻。

通过上面的 NIO 源码展示,我相信很多同学会发现使用 Java NIO 来进行开发,会比较的费劲:

  1. Java NIO 的类库和 API 比较复杂,我们需要熟练掌握相关类和接口的使用。
  2. Java NIO 的可靠性是比较低的,例如断开重连、半包问题和序列化都是需要开发者自己去搞定的。
  3. Java NIO 中有一个非常出名的 BUG,那就是关于 epoll 的 bug,它会导致 Selector 空轮询,最终导致 CPU 100%。

所以,如果我们进行 NIO 编程,都会首选 Netty 这款 NIO 框架。而至于 Netty 是如何的强大,那么就需要大家去自己体验和摸索了~

如何解读 Java IO、NIO 中的同步阻塞与同步非阻塞?的更多相关文章

  1. 一文理解Java IO/NIO/AIO

      目录 概述 一.IO流(同步.阻塞) 二.NIO(同步.非阻塞) 三.NIO2(异步.非阻塞) 正文 概述 在我们学习Java的IO流之前,我们都要了解几个关键词 同步与异步(synchronou ...

  2. java IO NIO BIO 最权威的总结

    1. BIO (Blocking I/O) 1.1 传统 BIO 1.2 伪异步 IO 1.3 代码示例 1.4 总结 2. NIO (New I/O) 2.1 NIO 简介 2.2 NIO的特性/N ...

  3. 【Java】NIO中Selector的select方法源码分析

    该篇博客的有些内容和在之前介绍过了,在这里再次涉及到的就不详细说了,如果有不理解请看[Java]NIO中Channel的注册源码分析, [Java]NIO中Selector的创建源码分析 Select ...

  4. 揭开Java IO流中的flush()的神秘面纱

    大家在使用Java IO流中OutputStream.PrintWriter --时,会经常用到它的flush()方法. 与在网络硬件中缓存一样,流还可以在软件中得到缓存,即直接在Java代码中缓存. ...

  5. java.io包中的字节流—— FilterInputStream和FilterOutputStream

    接着上篇文章,本篇继续说java.io包中的字节流.按照前篇文章所说,java.io包中的字节流中的类关系有用到GoF<设计模式>中的装饰者模式,而这正体现在FilterInputStre ...

  6. Java IO流中的File类学习总结

    一.File类概述 File类位于java.io包中,是对文件系统中文件以及文件夹进行封装的对象,可以通过对象的思想来操作文件和文件夹. File类有多种重载的构造方法.File类保存文件或目录的各种 ...

  7. 1.java.io包中定义了多个流类型来实现输入和输出功能,

    1.java.io包中定义了多个流类型来实现输入和输出功能,可以从不同的角度对其进行分 类,按功能分为:(C),如果为读取的内容进行处理后再输出,需要使用下列哪种流?(G)   A.输入流和输出流 B ...

  8. Java基础知识强化之多线程笔记07:同步、异步、阻塞式、非阻塞式 的联系与区别

    1. 同步: 所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回.但是一旦调用返回,就必须先得到返回值了. 换句话话说,调用者主动等待这个"调用"的结果. 对于 ...

  9. 阻塞式和非阻塞式IO

    有很多人把阻塞认为是同步,把非阻塞认为是异步:个人认为这样是不准确的,当然从思想上可以这样类比,但方式是完全不同的,下面说说在JAVA里面阻塞IO和非阻塞IO的区别 在JDK1.4中引入了一个NIO的 ...

随机推荐

  1. C 语言学习 说明

    因经常使用C++以及从未系统的学习C,导致最近写C的时候有种混沌感: 首先:对于哪些是C 的标准头文件尚不能很清晰的认知 其次:C 的某些函数属于哪个头文件,这个函数背后的原理实现较多不能理清 因此觉 ...

  2. python与pycharm的爱恨情仇

    首先大家应该区别 这两者是什么? python 是一门语言 pycharm 是工具 还得交待的是  可以编译python的工具 不止这一款 比如说--eclipse idea ... eclipse中 ...

  3. 【HDU5869】 Different GCD Subarray Query 题解 (线段树维护区间GCD)

    题目大意:求区间$[L,R]$中所有子区间产生的最大公因数的个数. ------------------------- 对于$gcd$,我们知道$gcd(a,b,c)=gcd(gcd(a,b),c)$ ...

  4. python4.4模块

    import random                         #import导入,random随机数模块a=random.random()                         ...

  5. 006_go语言中的互斥锁的作用练习与思考

    在go语言基本知识点中,我练习了一下互斥锁,感觉还是有点懵逼状,接下来为了弄懂,我再次进行了一些尝试,以下就是经过我的尝试后得出的互斥锁的作用. 首先还是奉上我改造后的代码: package main ...

  6. jquery 效果笔记

    jquery效果 显示隐藏     show()     语法     show([speed,[easing],[fn]])     参数可以省略,无动画直接使用     hide()     to ...

  7. pytorch载入模型的参数总是变化,比如说某个conv(3,3)kernel的几个参数总是变化:

  8. C++socket编程write()、read()简介及与send()、recv()的区别

    1. write 函数原型:ssize_t write(int fd, const void*buf,size_t nbytes)write函数将buf中的nbytes字节内容写入文件描述符fd.成功 ...

  9. 2020-05-18:MYSQL为什么用B+树做索引结构?平时过程中怎么加的索引?

    福哥答案2020-05-18:此答案来自群员:因为4.0成型那个年代,B树体系大量用于文件存储系统,甚至当年的Longhorn的winFS都是基于b树做索引,开源而且好用的也就这么个体系了.B+树的磁 ...

  10. Spring——AOP实现

    Spring实现AOP 1.什么是 AOP AOP (Aspect Orient Programming),直译过来就是 面向切面编程.AOP 是一种编程思想,是面向对象编程(OOP)的一种补充.面向 ...