如何解读 Java IO、NIO 中的同步阻塞与同步非阻塞?
原文链接:如何解读 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 模型:
阻塞 I/O 模型:缺省情况下,所有文件操作都是阻塞的。我们以套接字接口为例讲解此模型:在进程空间中调用 recvfrom,其系统调用知道数据包到达且被复制到应用进程的缓冲区中或者发生错误时才返回,在此期间一直会等待,进程在从调用 recvfrom 开始到它返回的整段时间内都是被阻塞的,因此被称为阻塞 I/O 模型。
非阻塞 I/O 模型:recvfrom 从应用层到内核的时候,如果该缓冲区没有数据的话,就直接返回一个 EWOULDBLOCK 错误,一般都对非阻塞 I/O 模型进行轮询检查这个状态,看内核是不是有数据到来。
I/O 复用模型:Linux 提供 select/poll 进程通过将一个或多个 fd 传递给 select 或 poll 系统调用,阻塞在 select 操作上,这样 select/poll 可以帮我们侦测多个 fd 是否处于就绪状态。select/poll 是顺序扫描 fd 是否就绪,而且支持的 fd 数量有限,因此它的使用收到了一下制约。Linux 还提供了一个 epoll 系统调用,epoll 使用基于事件驱动方式代替顺序扫描,因此性能更高。当有 fd 就绪时,立刻回调函数 rollback。
信号驱动 I/O 模型:首先开启套接口信号驱动 I/O 功能,并通过系统调用 sigaction 执行一个信号处理函数(此系统调用立刻返回,进程继续工作,它是非阻塞的)。当数据准备就绪时,就为该进程生成一个 SIGIO 信号,通过信号回调通知应用程序调用 recvfrom 来读取数据,并通知主循环函数处理数据。
异步 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 来进行开发,会比较的费劲:
- Java NIO 的类库和 API 比较复杂,我们需要熟练掌握相关类和接口的使用。
- Java NIO 的可靠性是比较低的,例如断开重连、半包问题和序列化都是需要开发者自己去搞定的。
- Java NIO 中有一个非常出名的 BUG,那就是关于 epoll 的 bug,它会导致 Selector 空轮询,最终导致 CPU 100%。
所以,如果我们进行 NIO 编程,都会首选 Netty 这款 NIO 框架。而至于 Netty 是如何的强大,那么就需要大家去自己体验和摸索了~
如何解读 Java IO、NIO 中的同步阻塞与同步非阻塞?的更多相关文章
- 一文理解Java IO/NIO/AIO
目录 概述 一.IO流(同步.阻塞) 二.NIO(同步.非阻塞) 三.NIO2(异步.非阻塞) 正文 概述 在我们学习Java的IO流之前,我们都要了解几个关键词 同步与异步(synchronou ...
- 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 ...
- 【Java】NIO中Selector的select方法源码分析
该篇博客的有些内容和在之前介绍过了,在这里再次涉及到的就不详细说了,如果有不理解请看[Java]NIO中Channel的注册源码分析, [Java]NIO中Selector的创建源码分析 Select ...
- 揭开Java IO流中的flush()的神秘面纱
大家在使用Java IO流中OutputStream.PrintWriter --时,会经常用到它的flush()方法. 与在网络硬件中缓存一样,流还可以在软件中得到缓存,即直接在Java代码中缓存. ...
- java.io包中的字节流—— FilterInputStream和FilterOutputStream
接着上篇文章,本篇继续说java.io包中的字节流.按照前篇文章所说,java.io包中的字节流中的类关系有用到GoF<设计模式>中的装饰者模式,而这正体现在FilterInputStre ...
- Java IO流中的File类学习总结
一.File类概述 File类位于java.io包中,是对文件系统中文件以及文件夹进行封装的对象,可以通过对象的思想来操作文件和文件夹. File类有多种重载的构造方法.File类保存文件或目录的各种 ...
- 1.java.io包中定义了多个流类型来实现输入和输出功能,
1.java.io包中定义了多个流类型来实现输入和输出功能,可以从不同的角度对其进行分 类,按功能分为:(C),如果为读取的内容进行处理后再输出,需要使用下列哪种流?(G) A.输入流和输出流 B ...
- Java基础知识强化之多线程笔记07:同步、异步、阻塞式、非阻塞式 的联系与区别
1. 同步: 所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回.但是一旦调用返回,就必须先得到返回值了. 换句话话说,调用者主动等待这个"调用"的结果. 对于 ...
- 阻塞式和非阻塞式IO
有很多人把阻塞认为是同步,把非阻塞认为是异步:个人认为这样是不准确的,当然从思想上可以这样类比,但方式是完全不同的,下面说说在JAVA里面阻塞IO和非阻塞IO的区别 在JDK1.4中引入了一个NIO的 ...
随机推荐
- 解决Jenkins的html样式不生效问题的终极方案
本文从四个步骤来分享我们在自行搭建jenkins过程中遇到的报表样式不全(即html报告展示不正确)的问题: 1.问题现象 2.问题原因 3.问题原因补充 4.解决方法(可以直接跳到第四步解决问题) ...
- Jenkins总结2-部署maven项目
1. 部署Maven项目 1.1 新建项目 选择新建任务 输入任务名称,并选择构建一个Maven项目.如果你的页面没有看到“构建一个maven项目”,则需要安装Maven Integration插件. ...
- 安装Hive 使用beeline 链接 出现 User: AAA is not allowed to impersonate BBB
AAA 指的是 hdfs 文件系统的用户 BBB 是hive 设置的 hiveserver2 配置文件中的登陆用户名 在hadoop 配置如下 <property> <name> ...
- Flink的流处理API(二)
一.Environment 1,getExecutionEnvironment getExecutionEnvironment会根据查询运行的方式决定返回什么样的运行环境,是最常用的一种创建执行环境的 ...
- [NLP]LSTM理解
简介 LSTM(Long short-term memory,长短期记忆)是一种特殊的RNN,主要是为了解决长序列训练过程中的梯度消失问题.以下先从RNN介绍. 简说RNN RNN(Recurrent ...
- 8月份Python招聘情况怎么样?Python爬取招聘数据,并进行分析
前言 拉勾招聘是专业的互联网求职招聘平台.致力于提供真实可靠的互联网招聘求职找工作信息.今天我们一起使用 python 采集拉钩的 python 招聘信息,分析一下找到高薪工作需要掌握哪些技术 开发环 ...
- .NETCore微服务探寻(三) - 远程过程调用(RPC)
前言 一直以来对于.NETCore微服务相关的技术栈都处于一个浅尝辄止的了解阶段,在现实工作中也对于微服务也一直没有使用的业务环境,所以一直也没有整合过一个完整的基于.NETCore技术栈的微服务项目 ...
- javascript数组笔记
1.数组 2.利用new创建数组 var arr= new Array(); 3.利用数组字面量创建数组 var 数组名=[]; 4.数组里面的数据叫 5.数组的索引(数组下标从0开始) 6.遍历数组 ...
- Java—io流之打印流、 commons-IO
打印流 打印流根据流的分类: 字节打印流 PrintStream 字符打印流 PrintWriter /* * 需求:把指定的数据,写入到printFile.txt文件中 * * 分析: * 1, ...
- 火题小战 B. barbeque
火题小战 B. barbeque 题目描述 \(Robbery\) 是一个大吃货(雾) 某个神奇的串由牛肉和青椒构成,于是\(Robbery\)购买了\(n\)个餐包来自己做这个串,每个餐包中有一些牛 ...