你彻底搞清楚“I/O多路复用”和“异步I/O”的前世今生
在网络的初期网民很少服务器完全无压力那时的技术也没有现在先进通常用一个线程来全程跟踪处理一个请求。因为这样最简单。其实代码实现大家都知道就是服务器上有个ServerSocket在某个端口监听接收到客户端的连接后会创建一个Socket并把它交给一个线程进行后续处理。线程主要从Socket读取客户端传过来的数据然后进行业务处理并把结果再写入Socket传回客户端。由于网络的原因Socket创建后并不一定能立刻从它上面读取数据可能需要等一段时间此时线程也必须一直阻塞着。在向Socket写入数据时也可能会使线程阻塞。这里准备了一个示例主要逻辑如下客户端创建20个Socket并连接到服务器上再创建20个线程每个线程负责一个Socket。服务器端接收到这20个连接创建20个Socket接着创建20个线程每个线程负责一个Socket。为了模拟服务器端的Socket在创建后不能立马读取数据让客户端的20个线程分别休眠5-10之间的一个随机秒数。客户端的20个线程会在第5秒到第10秒这段时间内陆陆续续的向服务器端发送数据服务器端的20个线程也会陆陆续续接收到数据。/** * author lixinjie * since 2019-05-07 */ public class BioServer { static AtomicInteger counter new AtomicInteger(0); static SimpleDateFormat sdf new SimpleDateFormat(“HH:mm:ss”); public static void main(String[] args) { try { ServerSocket ss new ServerSocket(); ss.bind(new InetSocketAddress(“localhost”, 8080)); while (true) { Socket s ss.accept(); processWithNewThread(s); } } catch (Exception e) { e.printStackTrace(); } } static void processWithNewThread(Socket s) { Runnable run () - { InetSocketAddress rsa (InetSocketAddress)s.getRemoteSocketAddress(); System.out.println(time() “-” rsa.getHostName() “:” rsa.getPort() “-” Thread.currentThread().getId() “:” counter.incrementAndGet()); try { String result readBytes(s.getInputStream()); System.out.println(time() “-” result “-” Thread.currentThread().getId() “:” counter.getAndDecrement()); s.close(); } catch (Exception e) { e.printStackTrace(); } }; new Thread(run).start(); } static String readBytes(InputStream is) throws Exception { long start 0; int total 0; int count 0; byte[] bytes new byte[1024]; //开始读数据的时间 long begin System.currentTimeMillis(); while ((count is.read(bytes)) -1) { if (start 1) { //第一次读到数据的时间 start System.currentTimeMillis(); } total count; } //读完数据的时间 long end System.currentTimeMillis(); return “wait” (start - begin) “ms,read” (end - start) “ms,total” total “bs”; } static String time() { return sdf.format(new Date()); } }/** * author lixinjie * since 2019-05-07/ public class Client { public static void main(String[] args) { try { for (int i 0; i 20; i) { Socket s new Socket(); s.connect(new InetSocketAddress(“localhost”, 8080)); processWithNewThread(s, i); } } catch (IOException e) { e.printStackTrace(); } } static void processWithNewThread(Socket s, int i) { Runnable run () - { try { //睡眠随机的5-10秒模拟数据尚未就绪 Thread.sleep((new Random().nextInt(6) 5) * 1000); //写1M数据为了拉长服务器端读数据的过程 s.getOutputStream().write(prepareBytes()); //睡眠1秒让服务器端把数据读完 Thread.sleep(1000); s.close(); } catch (Exception e) { e.printStackTrace(); } }; new Thread(run).start(); } static byte[] prepareBytes() { byte[] bytes new byte[10241024*1]; for (int i 0; i bytes.length; i) { bytes[i] 1; } return bytes; } }执行结果如下时间-IP:Port-线程Id:当前线程数 15:11:52-127.0.0.1:55201-10:1 15:11:52-127.0.0.1:55203-12:2 15:11:52-127.0.0.1:55204-13:3 15:11:52-127.0.0.1:55207-16:4 15:11:52-127.0.0.1:55208-17:5 15:11:52-127.0.0.1:55202-11:6 15:11:52-127.0.0.1:55205-14:7 15:11:52-127.0.0.1:55206-15:8 15:11:52-127.0.0.1:55209-18:9 15:11:52-127.0.0.1:55210-19:10 15:11:52-127.0.0.1:55213-22:11 15:11:52-127.0.0.1:55214-23:12 15:11:52-127.0.0.1:55217-26:13 15:11:52-127.0.0.1:55211-20:14 15:11:52-127.0.0.1:55218-27:15 15:11:52-127.0.0.1:55212-21:16 15:11:52-127.0.0.1:55215-24:17 15:11:52-127.0.0.1:55216-25:18 15:11:52-127.0.0.1:55219-28:19 15:11:52-127.0.0.1:55220-29:20 时间-等待数据的时间,读取数据的时间,总共读取的字节数-线程Id:当前线程数 15:11:58-wait5012ms,read1022ms,total1048576bs-17:20 15:11:58-wait5021ms,read1022ms,total1048576bs-13:19 15:11:58-wait5034ms,read1008ms,total1048576bs-11:18 15:11:58-wait5046ms,read1003ms,total1048576bs-12:17 15:11:58-wait5038ms,read1005ms,total1048576bs-23:16 15:11:58-wait5037ms,read1010ms,total1048576bs-22:15 15:11:59-wait6001ms,read1017ms,total1048576bs-15:14 15:11:59-wait6016ms,read1013ms,total1048576bs-27:13 15:11:59-wait6011ms,read1018ms,total1048576bs-24:12 15:12:00-wait7005ms,read1008ms,total1048576bs-20:11 15:12:00-wait6999ms,read1020ms,total1048576bs-14:10 15:12:00-wait7019ms,read1007ms,total1048576bs-26:9 15:12:00-wait7012ms,read1015ms,total1048576bs-21:8 15:12:00-wait7023ms,read1008ms,total1048576bs-25:7 15:12:01-wait7999ms,read1011ms,total1048576bs-18:6 15:12:02-wait9026ms,read1014ms,total1048576bs-10:5 15:12:02-wait9005ms,read1031ms,total1048576bs-19:4 15:12:03-wait10007ms,read1011ms,total1048576bs-16:3 15:12:03-wait10006ms,read1017ms,total1048576bs-29:2 15:12:03-wait10010ms,read1022ms,total1048576bs-28:1可以看到服务器端确实为每个连接创建一个线程共创建了20个线程。客户端进入休眠约5-10秒模拟连接上数据不就绪服务器端线程在等待等待时间约5-10秒。客户端陆续结束休眠往连接上写入1M数据服务器端开始读取数据整个读取过程约1秒。可以看到服务器端的工作线程会把时间花在“等待数据”和“读取数据”这两个过程上。这有两个不好的地方一是有很多客户端同时发起请求的话服务器端要创建很多的线程可能会因为超过了上限而造成崩溃。二是每个线程的大部分时光中都是在阻塞着无事可干造成极大的资源浪费。开头已经说了那个年代网民很少所以不可能会有大量请求同时过来。至于资源浪费就浪费吧反正闲着也是闲着。来个简单的小例子饭店共有10张桌子且配备了10位服务员。只要有客人来了大堂经理就把客人带到一张桌子并安排一位服务员全程陪同。即使客人暂时不需要服务服务员也一直在旁边站着。可能觉着是一种浪费其实非也这就是尊贵的VIP服务。其实VIP映射的是一对一的模型主要体现在“专用”上或“私有”上。真正的多路复用技术多路复用技术原本指的是在通信方面多种信号或数据从宏观上看交织在一起使用同一条传输通道进行传输。这样做的目的一方面可以充分利用通道的传输能力另一方面自然是省时省力省钱啦。其实这个概念非常的“生活化”随手就可以举个例子一条小水渠里水在流在一端往里倒入大量乒乓球在另一端用网进行过滤把乒乓球和水流分开。这就是一个比较“土”的多路复用首先在发射端把多种信号或数据进行“混合”接着是在通道上进行传输最后在接收端“分离”出自己需要的信号或数据。相信大家都看出来了这里的重点其实就是处理好“混合”和“分离”对于不同的信号或数据有不同的处理方法。比如以前的有线电视是模拟信号即电磁波。一家一般只有一根信号线但可以同时接多个电视每个电视任意换台互不影响。这是由于不同频率的波可以混合和分离。当然可能不是十分准确明白意思就行了。再比如城市的高铁站一般都有数个站台供高铁同时停靠但城市间的高铁轨道单方向只有一条如何保证那么多趟高铁安全运行呢很明显是分时使用每趟高铁都有自己的时刻。多趟高铁按不同的时刻出站相当于混合按不同的时刻进站相当于分离。总结一下多路指的是多种不同的信号或数据或其它事物复用指的是共用同一个物理链路或通道或载体。可见多路复用技术是一种一对多的模型“多”的这一方复用了“一”的这一方。其实一对多的模型主要体现在“公用”上或“共享”上。您先看着我一会再过来一对一服务是典型的有钱任性虽然响应及时、服务周到但不是每个人都能享受的毕竟还是“屌丝”多嘛那就来个共享服务吧。所以实际当中更多的情况是客人坐下后会给他一个菜单让他先看着反正也不可能立马点餐服务员就去忙别的了。可能不时的会有服务员从客人身旁经过发现客人还没有点餐就会主动去询问现在需要点餐吗如果需要服务员就给你写菜单如果不需要服务员就继续往前走了。这种情况饭店整体运行的也很好但是服务员人数少多了。现在服务10桌客人4个服务员绰绰有余。这节省的可都是纯利润呀。)因为10桌客人同时需要服务的情况几乎是不会发生的绝大部分情况都是错开的。如果真有的话那就等会好了又不是120/119人命关天的。回到代码里情况与之非常相似完全可以采用相同的理论去处理。连接建立后找个地方把它放到那里可以暂时先不管它反正此时也没有数据可读。但是数据早晚会到来的所以要不时的去询问每个连接有数据没有有的话就读取数据没有的话就继续不管它。其实这个模式在Java里早就有了就是Java NIO这里的大写字母“N”是单词“New”即“新”的意思主要是为了和上面的“一对一”进行区分。先铺垫一下吧现在需要把Socket交互的过程再稍微细化一些。客户端先请求连接connect服务器端然后接受连接accept然后客户端再向连接写入数据write接着服务器端从连接上读出数据read。和打电话的场景一样主叫拨号connect被叫接听accept主叫说话speak被叫聆听listen。主叫给被叫打电话说明主叫找被叫有事所以被叫关注的是接通电话听对方说。客户端主动向服务器端发起请求说明客户端找服务器端有事所以服务器端关注的是接受请求读取对方传来的数据。这里把接受请求读取数据称为服务器端感兴趣的操作。在Java NIO中接受请求的操作用OP_ACCEPT表示读取数据的操作用OP_READ表示。我决定先过一遍饭店的场景让首次接触Java NIO的同学不那么迷茫。就是把常规的场景进行了定向整理稍微有点刻意明白意思就行了。1、专门设立一个“跑腿”服务员工作职责单一就是问问客人是否需要服务。2、站在门口接待客人本来是大堂经理的工作但是他不愿意在门口盯着于是就委托给跑腿服务员你帮我盯着有人来了告诉我。于是跑腿服务员就有了一个任务替大堂经理盯梢。终于来客人了跑腿服务员赶紧告诉了大堂经理。3、大堂经理把客人带到座位上对跑腿服务员说客人接下来肯定是要点餐的但是现在在看菜单不知道什么时候能看好所以你不时的过来问问看需不需要点餐需要的话就再喊来一个“点餐”服务员给客人写菜单。于是跑腿服务员就又多了一个任务就是盯着这桌客人不时来问问如果需要服务的话就叫点餐服务员过来服务。4、跑腿服务员在某次询问中客人终于决定点餐了跑题服务员赶紧找来一个点餐服务员为客人写菜单。5、就这样跑腿服务员既要盯着门外新过来的客人也要盯着门内已经就坐的客人。新客人来了通知大堂经理去接待。就坐的客人决定点餐了通知点餐服务员去写菜单。事情就这样一直循环的持续下去一切都挺好。角色明确职责单一配合很好。大堂经理和点餐服务员是需求的提供者或实现者跑腿服务员是需求的发现者并识别出需求的种类需要接待的交给大堂经理需要点餐的交给点餐服务员。哈哈Java NIO来啦代码的写法非常的固定可以配合着后面的解说来看这样就好理解了如下/** * author lixinjie * since 2019-05-07 */ public class NioServer { static int clientCount 0; static AtomicInteger counter new AtomicInteger(0); static SimpleDateFormat sdf new SimpleDateFormat(“HH:mm:ss”); public static void main(String[] args) { try { Selector selector Selector.open(); ServerSocketChannel ssc ServerSocketChannel.open(); ssc.configureBlocking(false); ssc.register(selector, SelectionKey.OP_ACCEPT); ssc.bind(new InetSocketAddress(“localhost”, 8080)); while (true) { selector.select(); Set keys selector.selectedKeys(); Iterator iterator keys.iterator(); while (iterator.hasNext()) { SelectionKey key iterator.next(); iterator.remove(); if (key.isAcceptable()) { ServerSocketChannel ssc1 (ServerSocketChannel)key.channel(); SocketChannel sc null; while ((sc ssc1.accept()) ! null) { sc.configureBlocking(false); sc.register(selector, SelectionKey.OP_READ); InetSocketAddress rsa (InetSocketAddress)sc.socket().getRemoteSocketAddress(); System.out.println(time() “-” rsa.getHostName() “:” rsa.getPort() “-” Thread.currentThread().getId() “:” (clientCount)); } } else if (key.isReadable()) { //先将“读”从感兴趣操作移出待把数据从通道中读完后再把“读”添加到感兴趣操作中 //否则该通道会一直被选出来 key.interestOps(key.interestOps() (~ SelectionKey.OP_READ)); processWithNewThread((SocketChannel)key.channel(), key); } } } } catch (Exception e) { e.printStackTrace(); } } static void processWithNewThread(SocketChannel sc, SelectionKey key) { Runnable run () - { counter.incrementAndGet(); try { String result readBytes(sc); //把“读”加进去 key.interestOps(key.interestOps() | SelectionKey.OP_READ); System.out.println(time() “-” result “-” Thread.currentThread().getId() “:” counter.get()); sc.close(); } catch (Exception e) { e.printStackTrace(); } counter.decrementAndGet(); }; new Thread(run).start(); } static String readBytes(SocketChannel sc) throws Exception { long start 0; int total 0; int count 0; ByteBuffer bb ByteBuffer.allocate(1024); //开始读数据的时间 long begin System.currentTimeMillis(); while ((count sc.read(bb)) -1) { if (start 1) { //第一次读到数据的时间 start System.currentTimeMillis(); } total count; bb.clear(); } //读完数据的时间 long end System.currentTimeMillis(); return “wait” (start - begin) “ms,read” (end - start) “ms,total” total “bs”; } static String time() { return sdf.format(new Date()); } }

相关新闻

HarmonyOS开发实战:小分享-从EntryAbility看UIAbility生命周期

HarmonyOS开发实战:小分享-从EntryAbility看UIAbility生命周期

前言 在 HarmonyOS Stage 模型中,UIAbility 是承载 UI 的核心组件,它的生命周期回调决定了 App 启动、前台、后台、销毁的全过程。本篇以小分享 App 的 EntryAbility 为例,逐一拆解这些回调的触发时机与典型用法。掌握 UIAbility 生命周期是…

2026/10/16 17:07:12 阅读更多 →
技术写作规范优化:提升内容质量与SEO收录效果

技术写作规范优化:提升内容质量与SEO收录效果

这次我们来看一个技术写作规范优化的项目。这个项目的核心价值在于帮助技术作者快速掌握专业的内容创作规范,避免常见的写作陷阱,提升文章质量和平台收录效果。对于技术博主来说,最关心的往往是:规范是否清晰可执行?能…

2026/10/12 1:10:21 阅读更多 →
中国机遇2.0:数字经济、绿色科技与高端制造的新引擎

中国机遇2.0:数字经济、绿色科技与高端制造的新引擎

1. 项目概述:解码"中国机遇2.0"的全球价值 世界经济论坛新领军者年会(俗称"夏季达沃斯")今年最引人注目的议题莫过于"中国机遇2.0"概念的提出。这个升级版的发展框架,正在重构跨国企业的战略版图—…

2026/10/14 10:53:21 阅读更多 →

最新新闻

MSCDRUN.DLL丢失报错全解析:从原理到实操的完整修复指南

MSCDRUN.DLL丢失报错全解析:从原理到实操的完整修复指南

很多朋友可能都遇到过这种情况:正开着某个软件或者游戏,突然屏幕一弹,跳出一个“无法启动此程序,因为计算机中丢失MSCDRUN.DLL”或者“找不到MSCDRUN.DLL”的报错,接着软件就崩了,死活打不开。今天咱们就专…

2026/10/17 8:46:25 阅读更多 →
模型剪枝与蒸馏组合优化:从原理到工程实践

模型剪枝与蒸馏组合优化:从原理到工程实践

模型剪枝和蒸馏组合优化策略,这两个词放一起,懂行的都知道这是模型部署前最磨人的一道坎。我在实际项目里折腾这套东西的时间加起来得有上千个小时,踩过的坑比走过的路都多。从一开始天真地以为剪枝就是把不重要的权重置零,到后来…

2026/10/17 8:46:25 阅读更多 →
JS混淆逆向实战:从js_fuck到可调试代码的七步还原法

JS混淆逆向实战:从js_fuck到可调试代码的七步还原法

1. 项目概述:这不是“破解”,而是对前端加密逻辑的逆向工程实践“猿人学题库十四题——js_fuck核心代码加密”这个标题,乍看像一道CTF题目或爬虫对抗场景里的典型挑战,但它的实际价值远不止于“做题”。它指向一个在当前Web生态中…

2026/10/17 8:46:25 阅读更多 →
PVA引擎实战:优化自动驾驶CV流水线的数据流加速方案

PVA引擎实战:优化自动驾驶CV流水线的数据流加速方案

大概半年前,我们组里的感知迭代项目被一个问题卡了差不多两周:训练日志上显示计算设备利用率长期在85%以上,表面看一切都正常,但真正把一次"数据更新→重训→验证回归"走完,总时长却比按指标推算出来的结果慢…

2026/10/17 8:46:25 阅读更多 →
从零搭建图表设计系统:架构图、流程图与SVG渲染实践

从零搭建图表设计系统:架构图、流程图与SVG渲染实践

diagram-design 是我最近大半年一直在维护的一个内部项目,简单说,它是一套面向技术文档、架构评审和日常协作的图表设计系统。最初它只是某个工具里的几个 SVG 图元,后来不断被同事复制到白板、文档、PPT 里,颜色、线型、箭头口径…

2026/10/17 8:46:25 阅读更多 →
智能物流机器人整机ISTA 3E运输验证全流程详解

智能物流机器人整机ISTA 3E运输验证全流程详解

去年为一个智能物流机器人整机项目做包装验证时,我第一次系统性跑完了整套ISTA 3E程序。此前团队里大部分人习惯用ISTA 1G或者2系列的单件包装程序,结果在模拟运输阶段就暴露出了问题:机器人在托板上出现了明显滑移,机械臂关节锁销…

2026/10/17 8:45:25 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →