learn.lianglianglee.com/文章/Java NIO浅析.md.html
2022-05-11 18:52:13 +08:00

2079 lines
50 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>Java NIO浅析.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/文章/AQS 万字图文全面解析.md">AQS 万字图文全面解析.md.html</a>
</li>
<li>
<a href="/文章/Docker 镜像构建原理及源码分析.md">Docker 镜像构建原理及源码分析.md.html</a>
</li>
<li>
<a href="/文章/ElasticSearch 小白从入门到精通.md">ElasticSearch 小白从入门到精通.md.html</a>
</li>
<li>
<a href="/文章/JVM CPU Profiler技术原理及源码深度解析.md">JVM CPU Profiler技术原理及源码深度解析.md.html</a>
</li>
<li>
<a href="/文章/JVM 垃圾收集器.md">JVM 垃圾收集器.md.html</a>
</li>
<li>
<a href="/文章/JVM 面试的 30 个知识点.md">JVM 面试的 30 个知识点.md.html</a>
</li>
<li>
<a href="/文章/Java IO 体系、线程模型大总结.md">Java IO 体系、线程模型大总结.md.html</a>
</li>
<li>
<a class="current-tab" href="/文章/Java NIO浅析.md">Java NIO浅析.md.html</a>
</li>
<li>
<a href="/文章/Java 面试题集锦(网络篇).md">Java 面试题集锦(网络篇).md.html</a>
</li>
<li>
<a href="/文章/Java-直接内存 DirectMemory 详解.md">Java-直接内存 DirectMemory 详解.md.html</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决.md">Java中9种常见的CMS GC问题分析与解决.md.html</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决.md">Java中9种常见的CMS GC问题分析与解决.md.html</a>
</li>
<li>
<a href="/文章/Java中的SPI.md">Java中的SPI.md.html</a>
</li>
<li>
<a href="/文章/Java中的ThreadLocal.md">Java中的ThreadLocal.md.html</a>
</li>
<li>
<a href="/文章/Java线程池实现原理及其在美团业务中的实践.md">Java线程池实现原理及其在美团业务中的实践.md.html</a>
</li>
<li>
<a href="/文章/Java魔法类Unsafe应用解析.md">Java魔法类Unsafe应用解析.md.html</a>
</li>
<li>
<a href="/文章/Kafka 源码阅读笔记.md">Kafka 源码阅读笔记.md.html</a>
</li>
<li>
<a href="/文章/Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md">Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB Buffer Pool.md">MySQL · 引擎特性 · InnoDB Buffer Pool.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB IO子系统.md">MySQL · 引擎特性 · InnoDB IO子系统.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 事务系统.md">MySQL · 引擎特性 · InnoDB 事务系统.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 同步机制.md">MySQL · 引擎特性 · InnoDB 同步机制.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 数据页解析.md">MySQL · 引擎特性 · InnoDB 数据页解析.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB崩溃恢复.md">MySQL · 引擎特性 · InnoDB崩溃恢复.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · 临时表那些事儿.md">MySQL · 引擎特性 · 临时表那些事儿.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 半同步复制.md">MySQL 主从复制 半同步复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 基于GTID复制.md">MySQL 主从复制 基于GTID复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制.md">MySQL 主从复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 事务日志(redo log和undo log).md">MySQL 事务日志(redo log和undo log).md.html</a>
</li>
<li>
<a href="/文章/MySQL 亿级别数据迁移实战代码分享.md">MySQL 亿级别数据迁移实战代码分享.md.html</a>
</li>
<li>
<a href="/文章/MySQL 从一条数据说起-InnoDB行存储数据结构.md">MySQL 从一条数据说起-InnoDB行存储数据结构.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:事务和锁的面纱.md">MySQL 地基基础:事务和锁的面纱.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据字典.md">MySQL 地基基础:数据字典.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据库字符集.md">MySQL 地基基础:数据库字符集.md.html</a>
</li>
<li>
<a href="/文章/MySQL 性能优化:碎片整理.md">MySQL 性能优化:碎片整理.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md">MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:如何在日志中轻松定位大事务.md">MySQL 故障诊断:如何在日志中轻松定位大事务.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:教你快速定位加锁的 SQL.md">MySQL 故障诊断:教你快速定位加锁的 SQL.md.html</a>
</li>
<li>
<a href="/文章/MySQL 日志详解.md">MySQL 日志详解.md.html</a>
</li>
<li>
<a href="/文章/MySQL 的半同步是什么?.md">MySQL 的半同步是什么?.md.html</a>
</li>
<li>
<a href="/文章/MySQL中的事务和MVCC.md">MySQL中的事务和MVCC.md.html</a>
</li>
<li>
<a href="/文章/MySQL事务_事务隔离级别详解.md">MySQL事务_事务隔离级别详解.md.html</a>
</li>
<li>
<a href="/文章/MySQL优化优化 select count().md">MySQL优化优化 select count().md.html</a>
</li>
<li>
<a href="/文章/MySQL共享锁、排他锁、悲观锁、乐观锁.md">MySQL共享锁、排他锁、悲观锁、乐观锁.md.html</a>
</li>
<li>
<a href="/文章/MySQL的MVCC多版本并发控制.md">MySQL的MVCC多版本并发控制.md.html</a>
</li>
<li>
<a href="/文章/QingStor 对象存储架构设计及最佳实践.md">QingStor 对象存储架构设计及最佳实践.md.html</a>
</li>
<li>
<a href="/文章/RocketMQ 面试题集锦.md">RocketMQ 面试题集锦.md.html</a>
</li>
<li>
<a href="/文章/SnowFlake 雪花算法生成分布式 ID.md">SnowFlake 雪花算法生成分布式 ID.md.html</a>
</li>
<li>
<a href="/文章/Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md">Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html</a>
</li>
<li>
<a href="/文章/Spring Boot 教程:如何开发一个 starter.md">Spring Boot 教程:如何开发一个 starter.md.html</a>
</li>
<li>
<a href="/文章/Spring MVC 原理.md">Spring MVC 原理.md.html</a>
</li>
<li>
<a href="/文章/Spring MyBatis和Spring整合的奥秘.md">Spring MyBatis和Spring整合的奥秘.md.html</a>
</li>
<li>
<a href="/文章/Spring 帮助你更好的理解Spring循环依赖.md">Spring 帮助你更好的理解Spring循环依赖.md.html</a>
</li>
<li>
<a href="/文章/Spring 循环依赖及解决方式.md">Spring 循环依赖及解决方式.md.html</a>
</li>
<li>
<a href="/文章/Spring中眼花缭乱的BeanDefinition.md">Spring中眼花缭乱的BeanDefinition.md.html</a>
</li>
<li>
<a href="/文章/Vert.x 基础入门.md">Vert.x 基础入门.md.html</a>
</li>
<li>
<a href="/文章/eBay 的 Elasticsearch 性能调优实践.md">eBay 的 Elasticsearch 性能调优实践.md.html</a>
</li>
<li>
<a href="/文章/不可不说的Java“锁”事.md">不可不说的Java“锁”事.md.html</a>
</li>
<li>
<a href="/文章/互联网并发限流实战.md">互联网并发限流实战.md.html</a>
</li>
<li>
<a href="/文章/从ReentrantLock的实现看AQS的原理及应用.md">从ReentrantLock的实现看AQS的原理及应用.md.html</a>
</li>
<li>
<a href="/文章/从SpringCloud开始聊微服务架构.md">从SpringCloud开始聊微服务架构.md.html</a>
</li>
<li>
<a href="/文章/全面了解 JDK 线程池实现原理.md">全面了解 JDK 线程池实现原理.md.html</a>
</li>
<li>
<a href="/文章/分布式一致性理论与算法.md">分布式一致性理论与算法.md.html</a>
</li>
<li>
<a href="/文章/分布式一致性算法 Raft.md">分布式一致性算法 Raft.md.html</a>
</li>
<li>
<a href="/文章/分布式唯一 ID 解析.md">分布式唯一 ID 解析.md.html</a>
</li>
<li>
<a href="/文章/分布式链路追踪:集群管理设计.md">分布式链路追踪:集群管理设计.md.html</a>
</li>
<li>
<a href="/文章/动态代理种类及原理,你知道多少?.md">动态代理种类及原理,你知道多少?.md.html</a>
</li>
<li>
<a href="/文章/响应式架构与 RxJava 在有赞零售的实践.md">响应式架构与 RxJava 在有赞零售的实践.md.html</a>
</li>
<li>
<a href="/文章/大数据算法——布隆过滤器.md">大数据算法——布隆过滤器.md.html</a>
</li>
<li>
<a href="/文章/如何优雅地记录操作日志?.md">如何优雅地记录操作日志?.md.html</a>
</li>
<li>
<a href="/文章/如何设计一个亿级消息量的 IM 系统.md">如何设计一个亿级消息量的 IM 系统.md.html</a>
</li>
<li>
<a href="/文章/异步网络模型.md">异步网络模型.md.html</a>
</li>
<li>
<a href="/文章/当我们在讨论CQRS时我们在讨论些神马.md">当我们在讨论CQRS时我们在讨论些神马.md.html</a>
</li>
<li>
<a href="/文章/彻底理解 MySQL 的索引机制.md">彻底理解 MySQL 的索引机制.md.html</a>
</li>
<li>
<a href="/文章/最全的 116 道 Redis 面试题解答.md">最全的 116 道 Redis 面试题解答.md.html</a>
</li>
<li>
<a href="/文章/有赞权限系统(SAM).md">有赞权限系统(SAM).md.html</a>
</li>
<li>
<a href="/文章/有赞零售中台建设方法的探索与实践.md">有赞零售中台建设方法的探索与实践.md.html</a>
</li>
<li>
<a href="/文章/服务注册与发现原理剖析Eureka、Zookeeper、Nacos.md">服务注册与发现原理剖析Eureka、Zookeeper、Nacos.md.html</a>
</li>
<li>
<a href="/文章/深入浅出Cache.md">深入浅出Cache.md.html</a>
</li>
<li>
<a href="/文章/深入理解 MySQL 底层实现.md">深入理解 MySQL 底层实现.md.html</a>
</li>
<li>
<a href="/文章/漫画讲解 git rebase VS git merge.md">漫画讲解 git rebase VS git merge.md.html</a>
</li>
<li>
<a href="/文章/生成浏览器唯一稳定 ID 的探索.md">生成浏览器唯一稳定 ID 的探索.md.html</a>
</li>
<li>
<a href="/文章/缓存 如何保证缓存与数据库的双写一致性?.md">缓存 如何保证缓存与数据库的双写一致性?.md.html</a>
</li>
<li>
<a href="/文章/网易严选怎么做全链路监控的?.md">网易严选怎么做全链路监控的?.md.html</a>
</li>
<li>
<a href="/文章/美团万亿级 KV 存储架构与实践.md">美团万亿级 KV 存储架构与实践.md.html</a>
</li>
<li>
<a href="/文章/美团点评Kubernetes集群管理实践.md">美团点评Kubernetes集群管理实践.md.html</a>
</li>
<li>
<a href="/文章/美团百亿规模API网关服务Shepherd的设计与实现.md">美团百亿规模API网关服务Shepherd的设计与实现.md.html</a>
</li>
<li>
<a href="/文章/解读《阿里巴巴 Java 开发手册》背后的思考.md">解读《阿里巴巴 Java 开发手册》背后的思考.md.html</a>
</li>
<li>
<a href="/文章/认识 MySQL 和 Redis 的数据一致性问题.md">认识 MySQL 和 Redis 的数据一致性问题.md.html</a>
</li>
<li>
<a href="/文章/进阶Dockerfile 高阶使用指南及镜像优化.md">进阶Dockerfile 高阶使用指南及镜像优化.md.html</a>
</li>
<li>
<a href="/文章/铁总在用的高性能分布式缓存计算框架 Geode.md">铁总在用的高性能分布式缓存计算框架 Geode.md.html</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析.md">阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析.md">阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html</a>
</li>
<li>
<a href="/文章/面试最常被问的 Java 后端题.md">面试最常被问的 Java 后端题.md.html</a>
</li>
<li>
<a href="/文章/领域驱动设计在互联网业务开发中的实践.md">领域驱动设计在互联网业务开发中的实践.md.html</a>
</li>
<li>
<a href="/文章/领域驱动设计的菱形对称架构.md">领域驱动设计的菱形对称架构.md.html</a>
</li>
<li>
<a href="/文章/高效构建 Docker 镜像的最佳实践.md">高效构建 Docker 镜像的最佳实践.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>Java NIO浅析</h1>
<p>NIONon-blocking I/O在Java领域也称为New I/O是一种同步非阻塞的I/O模型也是I/O多路复用的基础已经被越来越多地应用到大型应用服务器成为解决高并发与大量连接、I/O处理问题的有效方式。</p>
<p>那么NIO的本质是什么样的呢它是怎样与事件模型结合来解放线程、提高系统吞吐的呢</p>
<p>本文会从传统的阻塞I/O和线程池模型面临的问题讲起然后对比几种常见I/O模型一步步分析NIO怎么利用事件模型处理I/O解决线程池瓶颈处理海量连接包括利用面向事件的方式编写服务端/客户端程序。最后延展到一些高级主题如Reactor与Proactor模型的对比、Selector的唤醒、Buffer的选择等。</p>
<p>注:本文的代码都是伪代码,主要是为了示意,不可用于生产环境。</p>
<h1>传统BIO模型分析</h1>
<p>让我们先回忆一下传统的服务器端同步阻塞I/O处理也就是BIOBlocking I/O的经典编程模型</p>
<pre><code class="language-text">{
ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//线程池
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(8088);
while(!Thread.currentThread.isInturrupted()){//主线程死循环等待新连接到来
Socket socket = serverSocket.accept();
executor.submit(new ConnectIOnHandler(socket));//为新的连接创建新的线程
}
class ConnectIOnHandler extends Thread{
private Socket socket;
public ConnectIOnHandler(Socket socket){
this.socket = socket;
}
public void run(){
while(!Thread.currentThread.isInturrupted()&amp;&amp;!socket.isClosed()){死循环处理读写事件
String someThing = socket.read()....//读取数据
if(someThing!=null){
......//处理数据
socket.write()....//写数据
}
}
}
}
</code></pre>
<p>这是一个经典的每连接每线程的模型之所以使用多线程主要原因在于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的当一个连接在处理I/O的时候系统是阻塞的如果是单线程的话必然就挂死在那里但CPU是被释放出来的开启多线程就可以让CPU去处理更多的事情。其实这也是所有使用多线程的本质</p>
<ol>
<li>利用多核。</li>
<li>当I/O阻塞系统但CPU空闲的时候可以利用多线程使用CPU资源。</li>
</ol>
<p>现在的多线程一般都使用线程池可以让线程的创建和回收成本相对较低。在活动连接数不是特别高小于单机1000的情况下这种模型是比较不错的可以让每一个连接专注于自己的I/O并且编程模型简单也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗可以缓冲一些系统处理不了的连接或请求。</p>
<p>不过,这个模型最本质的问题在于,严重依赖于线程。但线程是很&quot;&quot;的资源,主要表现在:</p>
<ol>
<li>线程的创建和销毁成本很高在Linux这样的操作系统中线程本质上就是一个进程。创建和销毁都是重量级的系统函数。</li>
<li>线程本身占用较大内存像Java的线程栈一般至少分配512K1M的空间如果系统中的线程数过千恐怕整个JVM的内存都会被吃掉一半。</li>
<li>线程的切换成本是很高的。操作系统发生线程切换的时候需要保留线程的上下文然后执行系统调用。如果线程数过高可能执行线程切换的时间甚至会大于线程执行的时间这时候带来的表现往往是系统load偏高、CPU sy使用率特别高超过20%以上),导致系统几乎陷入不可用的状态。</li>
<li>容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数一旦线程数量高但外部网络环境不是很稳定就很容易造成大量请求的结果同时返回激活大量阻塞线程从而使系统负载压力过大。</li>
</ol>
<p>所以当面对十万甚至百万级连接的时候传统的BIO模型是无能为力的。随着移动端应用的兴起和各种网络游戏的盛行百万级长连接日趋普遍此时必然需要一种更高效的I/O处理模型。</p>
<h1>NIO是怎么工作的</h1>
<p>很多刚接触NIO的人第一眼看到的就是Java相对晦涩的API比如ChannelSelectorSocket什么的然后就是一坨上百行的代码来演示NIO的服务端Demo……瞬间头大有没有</p>
<p>我们不管这些抛开现象看本质先分析下NIO是怎么工作的。</p>
<h2>常见I/O模型对比</h2>
<p>所有的系统I/O都分为两个阶段等待就绪和操作。举例来说读函数分为等待系统可读和真正的读同理写函数分为等待网卡可以写和真正的写。</p>
<p>需要说明的是等待就绪的阻塞是不使用CPU的是在“空等”而真正的读写操作的阻塞是使用CPU的真正在&quot;干活&quot;而且这个过程非常快属于memory copy带宽通常在1GB/s级别以上可以理解为基本不耗时。</p>
<p>下图是几种常见I/O模型的对比</p>
<p><img src="assets/v2-f47206d5b5e64448744b85eaf568f92d_1440w.jpg" alt="img" /></p>
<p>以socket.read()为例子:</p>
<p>传统的BIO里面socket.read()如果TCP RecvBuffer里没有数据函数会一直阻塞直到收到数据返回读到的数据。</p>
<p>对于NIO如果TCP RecvBuffer有数据就把数据从网卡读到内存并且返回给用户反之则直接返回0永远不会阻塞。</p>
<p>最新的AIO(Async I/O)里面会更进一步:不但等待就绪是非阻塞的,就连数据从网卡到内存的过程也是异步的。</p>
<p>换句话说BIO里用户最关心“我要读”NIO里用户最关心&quot;我可以读了&quot;在AIO模型里用户更需要关注的是“读完了”。</p>
<p>NIO一个重要的特点是socket主要的读、写、注册和接收函数在等待就绪阶段都是非阻塞的真正的I/O操作是同步阻塞的消耗CPU但性能非常高</p>
<h2>如何结合事件模型使用NIO同步非阻塞特性</h2>
<p>回忆BIO模型之所以需要多线程是因为在进行I/O操作的时候一是没有办法知道到底能不能写、能不能读只能&quot;傻等&quot;即使通过各种估算算出来操作系统没有能力进行读写也没法在socket.read()和socket.write()函数中返回这两个函数无法进行有效的中断。所以除了多开线程另起炉灶没有好的办法利用CPU。</p>
<p>NIO的读写函数可以立刻返回这就给了我们不开线程利用CPU的最好机会如果一个连接不能读写socket.read()返回0或者socket.write()返回0我们可以把这件事记下来记录的方式通常是在Selector上注册标记位然后切换到其它就绪的连接channel继续进行读写。</p>
<p>下面具体看下如何利用事件模型单线程处理所有I/O请求</p>
<p>NIO的主要事件有几个读就绪、写就绪、有新连接到来。</p>
<p>我们首先需要注册当这几个事件到来的时候所对应的处理器。然后在合适的时机告诉事件选择器我对这个事件感兴趣。对于写操作就是写不出去的时候对写事件感兴趣对于读操作就是完成连接和系统没有办法承载新读入的数据的时对于accept一般是服务器刚启动的时候而对于connect一般是connect失败需要重连或者直接异步调用connect的时候。</p>
<p>其次用一个死循环选择就绪的事件会执行系统调用Linux 2.6之前是select、poll2.6之后是epollWindows是IOCP还会阻塞的等待新事件的到来。新事件到来的时候会在selector上注册标记位标示可读、可写或者有连接到来。</p>
<p>注意select是阻塞的无论是通过操作系统的通知epoll还是不停的轮询(selectpoll)这个函数是阻塞的。所以你可以放心大胆地在一个while(true)里面调用这个函数而不用担心CPU空转。</p>
<p>所以我们的程序大概的模样是:</p>
<pre><code class="language-text"> interface ChannelHandler{
void channelReadable(Channel channel);
void channelWritable(Channel channel);
}
class Channel{
Socket socket;
Event event;//读,写或者连接
}
//IO线程主循环:
class IoThread extends Thread{
public void run(){
Channel channel;
while(channel=Selector.select()){//选择就绪的事件和对应的连接
if(channel.event==accept){
registerNewChannelHandler(channel);//如果是新连接,则注册一个新的读写处理器
}
if(channel.event==write){
getChannelHandler(channel).channelWritable(channel);//如果可以写,则执行写事件
}
if(channel.event==read){
getChannelHandler(channel).channelReadable(channel);//如果可以读,则执行读事件
}
}
}
Map&lt;ChannelChannelHandler&gt; handlerMap;//所有channel的对应事件处理器
}
</code></pre>
<p>这个程序很简短也是最简单的Reactor模式注册所有感兴趣的事件处理器单线程轮询选择就绪事件执行事件处理器。</p>
<h2>优化线程模型</h2>
<p>由上面的示例我们大概可以总结出NIO是怎么解决掉线程的瓶颈并处理海量连接的</p>
<p>NIO由原来的阻塞读写占用线程变成了单线程轮询事件找到可以进行读写的网络描述符进行读写。除了事件的轮询是阻塞的没有可干的事情必须要阻塞剩余的I/O操作都是纯CPU操作没有必要开启多线程。</p>
<p>并且由于线程的节约,连接数大的时候因为线程切换带来的问题也随之解决,进而为处理海量连接提供了可能。</p>
<p>单线程处理I/O的效率确实非常高没有线程切换只是拼命的读、写、选择事件。但现在的服务器一般都是多核处理器如果能够利用多核心进行I/O无疑对效率会有更大的提高。</p>
<p>仔细分析一下我们需要的线程,其实主要包括以下几种:</p>
<ol>
<li>事件分发器,单线程选择就绪的事件。</li>
<li>I/O处理器包括connect、read、write等这种纯CPU操作一般开启CPU核心个线程就可以。</li>
<li>业务线程在处理完I/O后业务一般还会有自己的业务逻辑有的还会有其他的阻塞I/O如DB操作RPC等。只要有阻塞就需要单独的线程。</li>
</ol>
<p>Java的Selector对于Linux系统来说有一个致命限制同一个channel的select不能被并发的调用。因此如果有多个I/O线程必须保证一个socket只能属于一个IoThread而一个IoThread可以管理多个socket。</p>
<p>另外连接的处理和读写的处理通常可以选择分开这样对于海量连接的注册和读写就可以分发。虽然read()和write()是比较高效无阻塞的函数但毕竟会占用CPU如果面对更高的并发则无能为力。</p>
<p><img src="assets/v2-22efc734724d07251f8293e2f1143639_1440w.png" alt="img" /></p>
<h1>NIO在客户端的魔力</h1>
<p>通过上面的分析可以看出NIO在服务端对于解放线程优化I/O和处理海量连接方面确实有自己的用武之地。那么在客户端上NIO又有什么使用场景呢?</p>
<p>常见的客户端BIO+连接池模型可以建立n个连接然后当某一个连接被I/O占用的时候可以使用其他连接来提高性能。</p>
<p>但多线程的模型面临和服务端相同的问题:如果指望增加连接数来提高性能,则连接数又受制于线程数、线程很贵、无法建立很多线程,则性能遇到瓶颈。</p>
<h2>每连接顺序请求的Redis</h2>
<p>对于Redis来说由于服务端是全局串行的能够保证同一连接的所有请求与返回顺序一致。这样可以使用单线程队列把请求数据缓冲。然后pipeline发送返回future然后channel可读时直接在队列中把future取回来done()就可以了。</p>
<p>伪代码如下:</p>
<pre><code class="language-text">class RedisClient Implements ChannelHandler{
private BlockingQueue CmdQueue;
private EventLoop eventLoop;
private Channel channel;
class Cmd{
String cmd;
Future result;
}
public Future get(String key){
Cmd cmd= new Cmd(key);
queue.offer(cmd);
eventLoop.submit(new Runnable(){
List list = new ArrayList();
queue.drainTo(list);
if(channel.isWritable()){
channel.writeAndFlush(list);
}
});
}
public void ChannelReadFinish(Channel channelBuffer Buffer){
List result = handleBuffer();//处理数据
//从cmdQueue取出future并设值future.done();
}
public void ChannelWritable(Channel channel){
channel.flush();
}
}
</code></pre>
<p>这样做能够充分的利用pipeline来提高I/O能力同时获取异步处理能力。</p>
<h2>多连接短连接的HttpClient</h2>
<p>类似于竞对抓取的项目往往需要建立无数的HTTP短连接然后抓取然后销毁当需要单机抓取上千网站线程数又受制的时候怎么保证性能呢?</p>
<p>何不尝试NIO单线程进行连接、写、读操作如果连接、读、写操作系统没有能力处理简单的注册一个事件等待下次循环就好了。</p>
<p>如何存储不同的请求/响应呢由于http是无状态没有版本的协议又没有办法使用队列好像办法不多。比较笨的办法是对于不同的socket直接存储socket的引用作为map的key。</p>
<h2>常见的RPC框架如ThriftDubbo</h2>
<p>这种框架内部一般维护了请求的协议和请求号可以维护一个以请求号为key结果的result为future的map结合NIO+长连接,获取非常不错的性能。</p>
<h1>NIO高级主题</h1>
<h2>Proactor与Reactor</h2>
<p>一般情况下I/O 复用机制需要事件分发器event dispatcher。 事件分发器的作用,即将那些读写事件源分发给各读写事件的处理者,就像送快递的在楼下喊: 谁谁谁的快递到了, 快来拿吧开发人员在开始的时候需要在分发器那里注册感兴趣的事件并提供相应的处理者event handler)或者是回调函数事件分发器在适当的时候会将请求的事件分发给这些handler或者回调函数。</p>
<p>涉及到事件分发器的两种模式称为Reactor和Proactor。 Reactor模式是基于同步I/O的而Proactor模式是和异步I/O相关的。在Reactor模式中事件分发器等待某个事件或者可应用或个操作的状态发生比如文件描述符可读写或者是socket可读写事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数由后者来做实际的读写操作。</p>
<p>而在Proactor模式中事件处理者或者代由事件分发器发起直接发起一个异步读写操作相当于请求而实际的工作是由操作系统来完成的。发起时需要提供的参数包括用于存放读到数据的缓存区、读的数据大小或用于存放外发数据的缓存区以及这个请求完后的回调函数等信息。事件分发器得知了这个请求它默默等待这个请求的完成然后转发完成事件给相应的事件处理者或者回调。举例来说在Windows上事件处理者投递了一个异步IO操作称为overlapped技术事件分发器等IO Complete事件完成。这种异步模式的典型实现是基于操作系统底层异步API的所以我们可称之为“系统级别”的或者“真正意义上”的异步因为具体的读写是由操作系统代劳的。</p>
<p>举个例子将有助于理解Reactor与Proactor二者的差异以读操作为例写操作类似</p>
<h4>在Reactor中实现读</h4>
<ul>
<li>注册读就绪事件和相应的事件处理器。</li>
<li>事件分发器等待事件。</li>
<li>事件到来,激活分发器,分发器调用事件对应的处理器。</li>
<li>事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。</li>
</ul>
<h4>在Proactor中实现读</h4>
<ul>
<li>处理器发起异步读操作注意操作系统必须支持异步IO。在这种情况下处理器无视IO就绪事件它关注的是完成事件。</li>
<li>事件分发器等待操作完成事件。</li>
<li>在分发器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自定义缓冲区,最后通知事件分发器读操作完成。</li>
<li>事件分发器呼唤处理器。</li>
<li>事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分发器。</li>
</ul>
<p>可以看出两个模式的相同点都是对某个I/O事件的事件通知即告诉某个模块这个I/O操作可以进行或已经完成)。在结构上两者也有相同点事件分发器负责提交IO操作异步)、查询设备是否可操作(同步)然后当条件满足时就回调handler不同点在于异步情况下Proactor)当回调handler时表示I/O操作已经完成同步情况下Reactor)回调handler时表示I/O设备可以进行某个操作can read 或 can write)。</p>
<p>下面我们将尝试应对为Proactor和Reactor模式建立可移植框架的挑战。在改进方案中我们将Reactor原来位于事件处理器内的Read/Write操作移至分发器不妨将这个思路称为“模拟异步”以此寻求将Reactor多路同步I/O转化为模拟异步I/O。以读操作为例子改进过程如下</p>
<ul>
<li>注册读就绪事件和相应的事件处理器。并为分发器提供数据缓冲区地址,需要读取数据量等信息。</li>
<li>分发器等待事件如在select()上等待)。</li>
<li>事件到来,激活分发器。分发器执行一个非阻塞读操作(它有完成这个操作所需的全部信息),最后调用对应处理器。</li>
<li>事件处理器处理用户自定义缓冲区的数据,注册新的事件(当然同样要给出数据缓冲区地址,需要读取的数据量等信息),最后将控制权返还分发器。
如我们所见通过对多路I/O模式功能结构的改造可将Reactor转化为Proactor模式。改造前后模型实际完成的工作量没有增加只不过参与者间对工作职责稍加调换。没有工作量的改变自然不会造成性能的削弱。对如下各步骤的比较可以证明工作量的恒定</li>
</ul>
<h4>标准/典型的Reactor</h4>
<ul>
<li>步骤1等待事件到来Reactor负责</li>
<li>步骤2将读就绪事件分发给用户定义的处理器Reactor负责</li>
<li>步骤3读数据用户处理器负责</li>
<li>步骤4处理数据用户处理器负责</li>
</ul>
<h4>改进实现的模拟Proactor</h4>
<ul>
<li>
<p>步骤1等待事件到来Proactor负责</p>
</li>
<li>
<p>步骤2得到读就绪事件执行读数据现在由Proactor负责</p>
</li>
<li>
<p>步骤3将读完成事件分发给用户处理器Proactor负责</p>
</li>
<li>
<p>步骤4处理数据用户处理器负责</p>
<p>对于不提供异步I/O API的操作系统来说这种办法可以隐藏Socket API的交互细节从而对外暴露一个完整的异步接口。借此我们就可以进一步构建完全可移植的平台无关的有通用对外接口的解决方案。</p>
</li>
</ul>
<p>代码示例如下:</p>
<pre><code class="language-text">interface ChannelHandler{
void channelReadComplate(Channel channelbyte[] data);
void channelWritable(Channel channel);
}
class Channel{
Socket socket;
Event event;//读,写或者连接
}
//IO线程主循环
class IoThread extends Thread{
public void run(){
Channel channel;
while(channel=Selector.select()){//选择就绪的事件和对应的连接
if(channel.event==accept){
registerNewChannelHandler(channel);//如果是新连接,则注册一个新的读写处理器
Selector.interested(read);
}
if(channel.event==write){
getChannelHandler(channel).channelWritable(channel);//如果可以写,则执行写事件
}
if(channel.event==read){
byte[] data = channel.read();
if(channel.read()==0)//没有读到数据,表示本次数据读完了
{
getChannelHandler(channel).channelReadComplate(channeldata;//处理读完成事件
}
if(过载保护){
Selector.interested(read);
}
}
}
}
Map&lt;ChannelChannelHandler&gt; handlerMap;//所有channel的对应事件处理器
}
</code></pre>
<h2>Selector.wakeup()</h2>
<h3>主要作用</h3>
<p>解除阻塞在Selector.select()/select(long)上的线程,立即返回。</p>
<p>两次成功的select之间多次调用wakeup等价于一次调用。</p>
<p>如果当前没有阻塞在select上则本次wakeup调用将作用于下一次select——“记忆”作用。</p>
<p>为什么要唤醒?</p>
<p>注册了新的channel或者事件。</p>
<p>channel关闭取消注册。</p>
<p>优先级更高的事件触发(如定时器事件),希望及时处理。</p>
<h3>原理</h3>
<p>Linux上利用pipe调用创建一个管道Windows上则是一个loopback的tcp连接。这是因为win32的管道无法加入select的fd set将管道或者TCP连接加入select fd set。</p>
<p>wakeup往管道或者连接写入一个字节阻塞的select因为有I/O事件就绪立即返回。可见wakeup的调用开销不可忽视。</p>
<h2>Buffer的选择</h2>
<p>通常情况下,操作系统的一次写操作分为两步:</p>
<ol>
<li>将数据从用户空间拷贝到系统空间。</li>
<li>从系统空间往网卡写。同理,读操作也分为两步:
① 将数据从网卡拷贝到系统空间;
② 将数据从系统空间拷贝到用户空间。</li>
</ol>
<p>对于NIO来说缓存的使用可以使用DirectByteBuffer和HeapByteBuffer。如果使用了DirectByteBuffer一般来说可以减少一次系统空间到用户空间的拷贝。但Buffer创建和销毁的成本更高更不宜维护通常会用内存池来提高性能。</p>
<p>如果数据量比较小的中小应用情况下可以考虑使用heapBuffer反之可以用directBuffer。</p>
<h1>NIO存在的问题</h1>
<p>使用NIO != 高性能,当连接数&lt;1000并发程度不高或者局域网环境下NIO并没有显著的性能优势。</p>
<p>NIO并没有完全屏蔽平台差异它仍然是基于各个操作系统的I/O系统实现的差异仍然存在。使用NIO做网络编程构建事件驱动模型并不容易陷阱重重。</p>
<p>推荐大家使用成熟的NIO框架如NettyMINA等。解决了很多NIO的陷阱并屏蔽了操作系统的差异有较好的性能和编程模型。</p>
<h1>总结</h1>
<p>最后总结一下到底NIO给我们带来了些什么</p>
<blockquote>
<ul>
<li>事件驱动模型</li>
<li>避免多线程</li>
<li>单线程处理多任务</li>
<li>非阻塞I/OI/O读写不再阻塞而是返回0</li>
<li>基于block的传输通常比基于流的传输更高效</li>
<li>更高级的IO函数zero-copy</li>
<li>IO多路复用大大提高了Java网络应用的可伸缩性和实用性</li>
</ul>
</blockquote>
<p>本文抛砖引玉诠释了一些NIO的思想和设计理念以及应用场景这只是从冰山一角。关于NIO可以谈的技术点其实还有很多期待未来有机会和大家继续探讨。</p>
<h3>作者简介</h3>
<p>王烨现在是美团旅游后台研发组的RD之前曾经在百度、去哪儿和优酷工作过专注Java后台开发。对于网络编程和并发编程具有浓厚的兴趣曾经做过一些基础组件也翻过一些源码属于比较典型的宅男技术控。期待能够与更多知己在coding的路上并肩前行~
联系邮箱:<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="c7b0a6a9a0bea2f7f487aaa2aeb3b2a6a9e9a4a8aa">[email&#160;protected]</a></p>
</div>
</div>
<div>
<div style="float: left">
<a href="/文章/Java IO 体系、线程模型大总结.md">上一页</a>
</div>
<div style="float: right">
<a href="/文章/Java 面试题集锦(网络篇).md">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997fb4eb898b66","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>