这两年,tomcat慢慢在新项目里不怎么接触了,由于都被spring boot之类的框架封装进了内部,成了内置server,不用像过去那样打个war包,再放到tomcat里部署了。java
可是,内部的机制咱们仍是有必要了解的,尤为是线程模型和classloader,这篇咱们会聚焦线程模型。redis
其实我本打算将一个问题,即你们知道,咱们平时最终写的controller、service那些业务代码,最终是由什么线程来执行的呢?spring
你们都是debug过的人,确定知道,线程名称大概以下:数据库
http-nio-8080-exec-2@5076
这个线程是tomcat的线程,假设,咱们在这个线程里,sleep个1分钟,模拟调用第三方服务时,第三方服务异常卡住不返回的状况,此时客户端每秒100个请求过来,此时整个程序会出现什么状况?后端
可是我发现,这个问题,一篇仍是讲不太清楚,所以,本篇只讲一下线程模型。tomcat
你们能够思考下,一个服务端程序,有哪些是确定须要的?app
咱们确定须要开启监听对吧,你们看看下面的bio程序:框架
这个就是个线程,在while(true)死循环里,一直accept客户端链接。异步
ok,这个线程确定是须要的。接下来,再看看仍是否须要其余的线程。socket
若是一切从简,咱们只用这1个线程也足够了,就像redis同样,redis都是内存操做,作啥都很快,还避免了线程切换的开销;
可是咱们的java后端,通常都要操做数据库的,这个是比较慢,天然是但愿把这部分工做可以交给单独的线程去作,在tomcat里,确实是这样的,交给了一个线程池,线程池里的线程,就是咱们平时看到的,名称相似http-nio-8080-exec-2@5076这样的,通常默认配置,最大200个线程。
但若是这样的话,1个acceptor + 一个业务线程池,会致使一个问题,就是,该acceptor既要负责新链接的接入,还要负责已接入链接的socket的io读写。假设咱们维护了10万个链接,这10万个链接都在不断地给咱们的服务端发数据,咱们服务端也在不停地给客户端返回数据,那这个工做仍是很繁重的,可能会压垮这个惟一的acceptor线程。
所以,理想状况下,咱们会在单独弄几个线程出来,负责已经接入的链接的io读写。
大致流程:
acceptor--->poller线程(负责已接入链接的io读写)-->业务线程池(http-nio-8080-exec-2@5076)
这个大概就是tomcat中的流程了。
在netty中,实际上是相似的:
boss eventloop--->worker eventloop-->通常在解码完成后的最后一个handler,交给自定义业务线程池
你们能够看看下图,这里面有几个橙色的方块,这几个表明了线程,从左到右,分别就是acceptor、nio线程池、poller线程。
1处,acceptor线程内部维护了一个endpoint对象,这个对象呢,就表明了1个服务端端点;该对象有几个实现类,以下:
咱们spring boot程序里,默认是用的NioEndpoint。
2处,将新链接交给NioEndpoint处理
@Override protected boolean setSocketOptions(SocketChannel socket) { // Process the connection try { // Disable blocking, polling will be used socket.configureBlocking(false); Socket sock = socket.socket(); socketProperties.setProperties(sock); // 进行一些socket的参数设置 NioSocketWrapper socketWrapper = new NioSocketWrapper(channel, this); channel.setSocketWrapper(socketWrapper); socketWrapper.setReadTimeout(getConnectionTimeout()); socketWrapper.setWriteTimeout(getConnectionTimeout()); //3 交给poller处理 poller.register(channel, socketWrapper); return true; } ... // Tell to close the socket return false; }
3处,就是交给NioEndpoint内部的poller对象去进行处理。
public void register(final NioChannel socket, final NioSocketWrapper socketWrapper) { socketWrapper.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into. PollerEvent r = null; // 丢到poller的队列里,poller线程会轮旋该队列 r = new PollerEvent(socket, OP_REGISTER); // 丢到队列里 addEvent(r); }
上面的addEvent值得一看。
private final SynchronizedQueue<PollerEvent> events = new SynchronizedQueue<>(); private void addEvent(PollerEvent event) { // 丢到队列里 events.offer(event); // 唤醒poller里的selector,及时将该socket注册到selector中 if (wakeupCounter.incrementAndGet() == 0) { selector.wakeup(); } }
到这里,acceptor线程的逻辑就结束了,一个异步放队列,完美收工。接下来,就是poller线程的工做了。
poller线程,要负责将该socket注册到selector里面去,而后还要负责该socket的io读写事件处理。
poller线程逻辑
public class Poller implements Runnable { private Selector selector; private final SynchronizedQueue<PollerEvent> events = new SynchronizedQueue<>();
能够看到,poller内部维护了一个selector,和一个队列,队列里也说了,主要是要新注册到selector的新socket。
既然丢到队列了,那咱们看看何时去队列取的呢?
@Override public void run() { // Loop until destroy() is called while (true) { boolean hasEvents = false; // 检查events hasEvents = events(); } }
这里咱们跟一下events()。
public boolean events() { boolean result = false; PollerEvent pe = null; for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++ ) { result = true; pe.run(); ... } return result; }
这里的
pe = events.poll()
就是去队列拉取事件,拉取到了以后,就会赋值给pe,而后下面就调用了pe.run方法。
pe的类型是PollerEvent,咱们看看其run方法会干啥?
@Override public void run() { if (interestOps == OP_REGISTER) { try { socket.getIOChannel().register(socket.getSocketWrapper().getPoller().getSelector(), SelectionKey.OP_READ, socket.getSocketWrapper()); } catch (Exception x) { log.error(sm.getString("endpoint.nio.registerFail"), x); } } }
这个方法难理解吗,看着有点吓人,其实就是把这个新的链接,向selector注册,感兴趣的io事件为OP_READ。后续呢,这个链接的io读写,就全由本poller的selector包了。
咱们说了,poller是个线程,在其runnable实现里,除了要处理上面的新链接注册到selector这个事,还要负责io读写,这部分逻辑就是在:
Iterator<SelectionKey> iterator=selector.selectedKeys().iterator(); while (iterator != null && iterator.hasNext()) { SelectionKey sk = iterator.next(); NioSocketWrapper socketWrapper = sk.attachment(); processKey(sk, socketWrapper); }
最后一行的processKey,会调用以下逻辑,将工做甩锅给http-nio-8080-exec-2@5076这类打杂的线程。
public boolean processSocket(SocketWrapperBase<S> socketWrapper,SocketEvent event, boolean dispatch) { Executor executor = getExecutor(); executor.execute(sc); return true; }
给个图的话,大概就是以下的红线流程部分了:
好了,到了课后思考时间了,咱们也说了,最终会交给http-nio-8080-exec-2@5076这类线程所在的线程池,那假设这些线程全都在sleep,会发生什么呢?
下一篇,咱们继续。