深入拆解 Tomcat & Jetty
李号双
eBay 技术主管
38890 人已学习
新⼈⾸单¥68
登录后,你可以任选4讲全文学习
课程目录
已完结/共 45 讲
开篇词 (1讲)
深入拆解 Tomcat & Jetty
15
15
1.0x
00:00/00:00
登录|注册

14 | NioEndpoint组件:Tomcat如何实现非阻塞I/O?

SocketWrapper
Http11Processor
SynchronizedQueue
Selector
PollerEvent
accept()
ServerSocketChannel
Sync
Executor
SocketProcessor
Poller
Acceptor
LimitLatch
总体工作流程
异步I/O
I/O多路复用
同步非阻塞I/O
同步阻塞I/O
用户线程与内核的I/O操作
异步I/O
信号驱动I/O
I/O多路复用
同步非阻塞I/O
同步阻塞I/O
NioEndpoint组件的名字与实现方式的疑问
Java并发编程技术的应用
NioEndpoint利用Java NIO API实现多路复用I/O模型
阻塞/非阻塞与同步/异步的区别
I/O模型解决内存与外部设备速度差异问题
高并发思路
NioEndpoint组件
Java I/O模型
UNIX系统下的I/O模型
课后思考
总结
参考文章

该思维导图由 AI 生成,仅供参考

UNIX 系统下的 I/O 模型有 5 种:同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O。这些名词我们好像都似曾相识,但这些 I/O 通信模型有什么区别?同步和阻塞似乎是一回事,到底有什么不同?等一下,在这之前你是不是应该问自己一个终极问题:什么是 I/O?为什么需要这些 I/O 模型?
所谓的 I/O 就是计算机内存与外部设备之间拷贝数据的过程。我们知道 CPU 访问内存的速度远远高于外部设备,因此 CPU 是先把外部设备的数据读到内存里,然后再进行处理。请考虑一下这个场景,当你的程序通过 CPU 向外部设备发出一个读指令时,数据从外部设备拷贝到内存往往需要一段时间,这个时候 CPU 没事干了,你的程序是主动把 CPU 让给别人?还是让 CPU 不停地查:数据到了吗,数据到了吗……
这就是 I/O 模型要解决的问题。今天我会先说说各种 I/O 模型的区别,然后重点分析 Tomcat 的 NioEndpoint 组件是如何实现非阻塞 I/O 模型的。

Java I/O 模型

对于一个网络 I/O 通信过程,比如网络数据读取,会涉及两个对象,一个是调用这个 I/O 操作的用户线程,另外一个就是操作系统内核。一个进程的地址空间分为用户空间和内核空间,用户线程不能直接访问内核空间。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

Tomcat的NioEndpoint组件利用Java NIO API实现了多路复用I/O模型,实现了非阻塞I/O。文章深入介绍了UNIX系统下的5种I/O模型,包括同步阻塞I/O、同步非阻塞I/O、I/O多路复用、信号驱动I/O和异步I/O,并详细解释了它们的区别和实现方式。在Tomcat的NioEndpoint组件中,通过LimitLatch控制连接数,Acceptor负责接收新连接,Poller作为Selector不断检测Channel的数据就绪状态,SocketProcessor任务对象由Poller生成并交给Executor处理。文章还探讨了高并发处理的思路,强调了合理设计线程模型和使用专门的线程池来处理不同任务。通过深入分析Tomcat的NioEndpoint组件实现原理,展示了其在处理I/O时运用到的Java并发编程技术,为读者提供了对各种I/O模型特点和Tomcat非阻塞I/O实现方法的快速了解。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《深入拆解 Tomcat & Jetty 》
新⼈⾸单¥68
立即购买
登录 后留言

全部留言(135)

  • 最新
  • 精选
  • 🐛
    老师,操作系统级的连接指的是什么啊?

    作者回复: TCP三次握手建立连接的过程中,内核通常会为每一个LISTEN状态的Socket维护两个队列: SYN队列(半连接队列):这些连接已经接到客户端SYN; ACCEPT队列(全连接队列):这些连接已经接到客户端的ACK,完成了三次握手,等待被accept系统调用取走。 Acceptor负责从ACCEPT队列中取出连接,当Acceptor处理不过来时,连接就堆积在ACCEPT队列中,这个队列长度也可以通过参数设置。

    2019-06-11
    76
  • Geek_28b75e
    问一个基础问题,线程的同步,和本节所讲的同步,意义上的不同

    作者回复: 不是一个概念,线程的同步一般指对共享变量的互斥访问。IO模型的同步是指应用和内核的交互方式。

    2019-06-11
    41
  • QQ怪
    老师,信号驱动式 I/O与其他io模型的有啥不一样?

    作者回复: 可以把信号驱动io理解为“半异步”,非阻塞模式是应用不断发起read调用查询数据到了内核没有,而信号驱动把这个过程异步化了,应用发起read调用时注册了一个信号处理函数,其实是个回调函数,数据到了内核后,内核触发这个回调函数,应用在回调函数里再发起一次read调用去读内核的数据。所以是半异步。

    2019-06-13
    35
  • BingoJ
    老师,那我们常说Java中自身的NIO到底是同步非阻塞,还是IO多路复用呢?

    作者回复: NIO API可以不用Selector,就是同步非阻塞。使用了Selector就是IO多路复用

    2019-06-21
    2
    25
  • 你好旅行者
    对于【同步与异步指的是应用程序在与内核通信时,数据从内核空间到应用空间的拷贝内核主动发起还是应用程序触发。】我有一个问题,以同步非阻塞为例,当网卡接收到数据,要将数据送到用户进程时,此时是由用户进程主动向操作系统请求拷贝网卡的数据吗?老师能不能详细介绍一下这个过程?

    作者回复: 是用户进程主动发起read调用,这个是一个系统调用,CPU由用户态切换到内核态,执行内核代码,内核发现这个socket上的数据已经到到了内核空间,将用户线程挂起,然后把数据从内核空间拷贝到用户空间,再唤醒用户线程,read调用返回。

    2019-06-17
    4
    25
  • Dovelol
    老师,请教下,”当客户端发起一个http请求时,首先由Acceptor线程run方法中的socket = endpoint.serverSocketAccept();接收连接,然后传递给名称为Poller的线程去侦测I/O事件,Poller线程会一直select,选出内核将数据从网卡拷贝到内核空间的 channel(也就是内核已经准备好数据)然后交给名称为Catalina-exec的线程去处理,这个过程也包括内核将数据从内核空间拷贝到用户空间这么一个过程,所以对于exec线程是阻塞的,此时用户空间(也就是exec线程)就接收到了数据,可以解析然后做业务处理了。 1.想问下老师我对这个流程的理解对吗,如果不对,哪个地方有问题呢? 2.老师讲的2个步骤是融合在这里面的吗? 3.老师说的“当用户线程发起 I/O 操作后,xxx”,这里面应该是哪一步去发起的I/O操作呢?

    作者回复: 1,理解的很准确 2,对的 3,Selector发出的select调用就是一个I/O操作。

    2019-06-12
    2
    20
  • zyz
    老师!Tomcat为什么不用Semaphore而是自己实现LimitLatch来限流呢?出于什么考虑?性能?不想强依赖Semaphore?

    作者回复: 可能是Semaphore的实现比较复杂,LimitLatch简单明了够用。

    2019-06-20
    18
  • Monday
    阻塞与同异步的区别 本节的总结有如下的2句话,1)阻塞与非阻塞指的是应用程序发起i/o操作后是等待还是立即返回。2)同步与异步指的是应用程序在与内核通信时,数据从内核空间到应用空间的拷贝内核主动发起还是应用程序触发。 1,阻塞对应的是等待,非阻塞对应的是立即返回。这句应该好理解。 2,同步对应的是哪个? 3,我的理解是js中ajax请求的有个属性,async为true异步false同步。这个对应了网络IO。好理解 4,我的理解阻塞非阻塞是java的jcu包下ArrayBlockingQueue队列中的offer和put方法的区别。其中前者是非阻塞的,队列满了就直接返回入队失败;后者是阻塞的,如果队列满了就阻塞入队的线程,直到队列有空闲并插入成功后返回true。这里面会牵涉到内核吗? 5,反正学完本节发现不知道的更多了,原来自己一直没分清楚过同/异步和是否阻塞。。。疼疼疼

    作者回复: 同步异步可以理解为谁主动,同步就是A问B要东西,总是A主动”伸手“问B要。异步就是A向B注册一个需求,货到了B主动“伸手”把货交给A。 阻塞队列在阻塞一个线程时,会有系统调用,有系统调用内核就要参与,只是这里的阻塞跟IO的阻塞是两回事。 其实不要迷茫,理解上面那几张图就行了。😑

    2019-06-12
    17
  • 大卫
    李老师您好, 我来结合实际问题提3个问题吧。 结合这篇文章请教下线上的遇到的故障,接口响应慢最终导致无法正常响应。 jstack打印了堆栈信息,其中一台台机器,总线程数一共4700多,有3500多个Runnable状态的线程,有WAITING状态的1200多。 另外一台机器总线程数一共1200多,大部分都是WAITING状态。 部署方式是物理机(高配置,一般是24CPU 128G内存)+docker(一个docker) springboot-tomcat参数设置如下: server.tomcat.accept-count=1000 server.tomcat.max-threads=1000 server.tomcat.max-connections=1000 其中3500多个Runnable状态的线程栈信息如下所示: "I/O dispatcher 1098713" #1202019 prio=5 os_prio=0 tid=0x00007fdd24071000 nid=0x221f runnable [0x00007fdd28709000] java.lang.Thread.State: RUNNABLE at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method) at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:269) at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:93) at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86) - locked <0x00000007504d4020> (a sun.nio.ch.Util$2) - locked <0x00000007504d4008> (a java.util.Collections$UnmodifiableSet) - locked <0x0000000754258eb8> (a sun.nio.ch.EPollSelectorImpl) at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97) at org.apache.http.impl.nio.reactor.AbstractIOReactor.execute(AbstractIOReactor.java:255) at org.apache.http.impl.nio.reactor.BaseIOReactor.execute(BaseIOReactor.java:104) at org.apache.http.impl.nio.reactor.AbstractMultiworkerIOReactor$Worker.run(AbstractMultiworkerIOReactor.java:588) at java.lang.Thread.run(Thread.java:745) 问题1:引起上述大量Runnable线程的原因,是因为tomcat最大连接数默认10000导致,放进来这么多待运行的线程吗?有疑惑,头一次遇到呢。 问题2:有1000多个WAITING状态的,目前分析是因为使用了HttpClient连接池,httpclient版本是4.5.5,其中可能没有合理设置超时参数导致,需要增加connectionRequestTimeout,减少retry重试次数,包括defaultMaxPerRoute参数的合理设置。这里的HttpClient线程池的大小和路由池大小怎么设置更合理呢?(可能跟tomcat没有太直接关系,如果老师有这方面经验不吝赐教) 问题3:上面提到部署方式是docker,其中一台docker压测接口比如qps是100,该接口就是调用了第三方接口聚合下返回结果,但是再在同一台物理机上部署一个docker,nginx负载到这两台docker上,qps还是100,并没有什么提升,这个应该从哪方面分析下原因呢? 问题有点多哦,麻烦老师了~

    作者回复: 从你的描述以及线程栈来看,瓶颈不在当前这个系统,而在于httpclient调用的下游服务响应比较慢,这能解释为什么大量AbstractIOReactor线程在等待响应数据。 1. 注意到配置max-connection为1000,acceptcount为1000,所以最大并发连接数是2000,不是10000。 2,1000多个waiting线程主要还是因为下游慢 3,正因为瓶颈不在当前系统,再横向扩展当前系统也无济于事,需要优化下游的响应时间或者横向扩展下游服务实例。

    2019-06-12
    2
    13
  • 飞翔
    ,当你的程序通过 CPU 向外部设备发出一个读指令时,数据从... 李老师想问一个问题, cpu发出读指令, 那么是什么东西负责读数据从硬盘到内存这个过程呢? 不是cpu嘛?

    作者回复: Direct Memory Access(存储器直接访问)。这是指一种高速的数据传输操作,允许在外部设备和存储器之间直接读写数据,既不通过CPU,也不需要CPU干预。整个数据传输操作在一个称为"DMA控制器"的控制下进行的。CPU除了在数据传输开始和结束时做一点处理外,在传输过程中CPU可以进行其他的工作

    2019-06-11
    12
收起评论
显示
设置
留言
99+
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部