我们在上一部分文章里已经看到了,Tomcat的架构是如何一步步构建出来,但是在后台服务器的构建中,一个很重要的问题是如何实现多线程?一般情况下,如果我们来实现最初步的想法就是:不断循环接收客户端的连接,每个连接构建一个线程,然后进行相关的数据处理!但是,实际上我们应该考虑的更多,比如如何选用BIO还是NIO等等?所以,我们来借鉴一下Tomcat的多线程设计思路!
首先Tomcat的设计了专门的连接线程,即所有的客户端连接都通过这类线程来处理,一旦建立连接就把获得的客户端socket交由轮询线程来处理,轮询各个客户端后再对已经准备好的客户端构建一个线程,交由线程池来处理Request和Response!如果你看源码将会发现实际上线程模型的基础AbstractEndpoint,每一种实现包含一种连接、和数据处理方式,我们以NioEndpoint为例,它包含LimitLatch、Acceptor、Poller、SocketProcessor、Excutor5个部分。LimitLatch是连接控制器,它负责维护连接数的计算,nio模式下默认是10000,达到这个阈值后,就会拒绝连接请求。Acceptor负责接收连接,默认是1个线程来执行,将请求的事件注册到事件列表。有Poller来负责轮询,Poller线程数量是cpu的核数Math.min(2,Runtime.getRuntime().availableProcessors())。由Poller将就绪的事件生成SocketProcessor同时交给Excutor去执行。在Excutor的线程中,会完成从socket中读取http request,解析成HttpServletRequest对象,分派到相应的servlet并完成逻辑,然后将response通过socket发回client。一个网上广泛引用的图例如下图:
假设一共有四个客户端连接到了服务器并请求数据,那么这个线程模型是怎么工作的呢?我们以下图为例,作为对上述抽象出来的模型的解说:
1、首先主线程通过NIO模式建立服务器绑定,并采用阻塞的模式
2、然后生成Acceptor线程,这里假设就一个线程,则这个线程将执行while循环监听客户端的连接请求;
3、启动Poller线程(实际上Poller和Acceptor线程几乎同时启动,因为多线程在未做同步的时候就是默认是异步的,当Acceptor.start执行之后,可以立即执行Poller.start),假设是双核处理器,则我们将生成两个Poller线程;如果连接成功,则立即把已经连接的客户端socket注册到随机Poller中,同时NioChannel对象封装在一个PollerEvent对象中,并将PollerEvent对象压入events queue里。这里是个典型的生产者-消费者模式,Acceptor与Poller线程之间通过queue通信,Acceptor是events queue的生产者,Poller是events queue的消费者。图例展示的是client1、client2被随机注册到Poller1的线程中;client3、client4被随机注册到Poller2线程中;这里所说的注册实际上是注册到Poller对象维护的Events队列中,然后在通过NIO模型中的"注册"注册到select选择器中,这里一个Poller维护一个select并对select注册的NioChannel发起轮询,凡是准备好的Channel则交由Thread_pool线程池;
4、在线程池中,线程拿到Poller传过来的socket后,将socket封装在SocketProcessor对象中。然后从Http11ConnectionHandler中取出Http11NioProcessor对象,从Http11NioProcessor中调用CoyoteAdapter的逻辑,跟BIO实现一样。在Worker线程中,会完成从socket中读取http request,解析成HttpServletRequest对象,分派到相应的servlet并完成逻辑,然后将response通过socket发回client。
最近手残,搞了个公众号,主要闲暇时间随便聊一些程序圈的一些事,也会分享一些技术面试的资料,感兴趣的可以关注一波。关注后,后台发送 面试指南,可以获取2021最新JAVA面试总结,基本看完后,JAVA八股文这些应该不在话下了。