Tomcat如何实现非阻塞IO
UNIX下的IO模式
- 同步IO
- 同步非阻塞IO
- IO多路复用
- 信号驱动IO
- 异步IO
Java的IO模型
对于一个网络IO通信过程,比如网络读取数据,会涉及两个对象
- 调用IO操作的用户线程
- 操作系统内核
一个进程的地址空间分为
用户空间和内核空间,用户线程不能直接访问内核空间
当用户线程发起IO操作后,网络数据读取操作会经历两个步骤
- 用户线程等待内核将数据从网卡拷贝到内核空间
- 内核将数据从内核空间拷贝到用户空间
各种IO模型的区别就是:这两个步骤的方式不一样
同步阻塞IO
- 1.用户线程发起read调用后就阻塞了,让出CPU.
- 2.内核等待网卡数据到来,把数据从网卡拷贝到内核空间
- 3.把数据拷贝到用户空间
- 4.唤醒用户线程

同步非阻塞IO
- 1.用户线程不断发起read调用,数据没有到内核空间时,每次都返回失败
- 2.直到数据到了内核空间,这一次read调用后,等待数据从内核空间拷贝到用户空间,线程处于阻塞状态
- 3.数据拷贝结束后,用户空间唤醒线程

IO多路复用
用户线程的读取操作分为两部分.
- 1.线程发起select调用,询问内核数据是否准备完成
- 2.等待内核数据准备好数据,用户发起read调用
- 3.等待数据从内核空间拷贝到内核空间,线程处于阻塞状态
但是因为select调用可以向内核查询多个channel的状态,所以叫多路复用

异步IO
- 1.用户线程发起read调用,并注册一个回调,read立即返回
- 2.内核数据准备好之后,调用指定的回调函数进行处理
整个过程用户线程不会阻塞

Tomcat的NioEndPoint组件
Tomcat的NioEndPoint组件实现了IO多路复用模型
Java多路复用器使用步骤
- 1.创建一个
Selector,然后注册各种事件监听,然后调用select()方法,等待事件触发. - 2.当事件触发后,例如可读,然后就可以创建线程从Channel中读取数据
Tomcat中的NioEndpoint组件实现较为复杂,但是基本原理和Java使用多路复用的原理是一致的.
NioEndPoint所包含的子组件
- LimitLatch,连接控制器,负责控制最大连接数.超过连接阈值后,连接请求被拒绝
- Acceptor,一个单独的线程用于轮询接收新的连接,一旦有新连接,accept方法就会返回一个Channel对象,将Channel对象交给Poller处理
- Poller,本质上是一个
Selector,也是在一个单独的线程哩,Poller内部维护Channel数组,不断检测Channel的数据就绪状态,一旦Channel可读,就生成一个SocketProcessor任务交给Executor执行 - SocketProcessor,实现Runnable接口,经过包装的具体执行方法
- Executor,任务执行线程池,负责执行SocketProcessor,SocketProcessor的run方法会调用
Http11Processor来读取和解析请求数据.

LimitLatch
LimitLatch控制连接数,当连接数到达最大时阻塞线程,直到后续组件处理完一个连接后将连接数-1.
从代码层面看,LimitLatch内部维护的内部类Sync,拓展了AQS,用于实现Tomcat的连接数控制.
Acceptor
Acceptor是EndPoint的内部类,实现了Runnable接口,因此可以在单独的线程中执行.
Accpetor是一个接口,而具体的实现类也是具体EndPoint中
由于一个端口号只对应一个ServerSocketChannel,所以这个ServerSocketChannel是多个Acceptor线程共享的,由EndPoint初始化.
serverSock = AsynchronousServerSocketChannel.open(threadGroup);
//..略
serverSock.bind(addr, getAcceptCount());
//设置阻塞式连接
serverSock.configureBlocking(true);
- 1.bind方法设置了操作系统等待队列长度.当应用层面连接数到达最大值时,操作系统可以继续接收连接,而最大能够继续接收的连接数就是这个队列长度.
- 2.ServerSocketChannel设置成阻塞模式,代表是阻塞的方式接收连接.
ServerSocketChannel通过accept()接受新连接,并返回一个SocketChannel对象,然后将SocketChannel对象封装在一个PollerEvent对象中,然后再将PollerEvent放入队列,一个典型的生产者-消费者模型,Acceptor与Poller线程之间通过队列通信
Poller
Poller本质上是一个Selector,内部维护一个队列,并且使用了SynchronizedQueue队列.
SynchronizedQueue的方法都是用的synchronized关键字修饰,保证并发读写的数据一致性. Poller不断的通过内部的Selector对象向内核查询Channel的状态,一旦可读就生成任务类SocketProcessor交给Executor处理. 同事Poller还有一个重要任务就是会遍历所有管理的SocketChannel是否超时,如果超时就会关闭这个SocketChannel
SocketProcessor
Poller创建的SocketProcessor任务类会交由线程池处理,而SocketProcessor实现了Runnable接口,用来定义Executor中真正执行的任务.
Executor
Executor是基于Java中ThreadPoolExector定制的线程池,负责真正执行SocketProcessor中的run方法,也就是解析请求并通过容器处理请求,最终调用Servlet.
高并发思路
高并发指的是能够在一定时间内快速地处理大量请求,所以就需要合理设计线程模型让CPU有序地忙起来,并且尽量不让线程阻塞,因为一旦阻塞,CPU就闲下来了.
NioEndPoint通过将接收连接,检测IO事件,处理请求用不同规模的线程去处理,并且能针对性的对线程池进行参数控制,这样就能达到高效地利用线程池和CPU的高效运行.
IO模型小结
IO模型是为了解决内存和外部设备读写速度差异的问题.
平时所说的阻塞或非阻塞是指应用在发起IO操作时,是立即返回还是等待,而同步和异步,指的是应用程序在与内核通信时,数据从内核空间到应用空间的拷贝,是由内核主动发起还是由应用程序来触发
Tomcat的NioEndPoint组件,到底非阻塞的还是多路复用
从NioEndPoint的命名来看,似乎说的是使用同步非阻塞IO模型,但是NioEndPoint又是调用Java的Selector实现,Selector实际上又是对应的IO多路复用.