这两天在看一本tomcat的书,书中讲到了无论是BIO还是NIO模式,Tomcat都会做连接限制的,想来也是这个道理,毕竟能让服务稳定运行才是最重要的。于是引出了一个陌生的类LimitLatch,下面就来看一看。
首先我们需要知道的是在tomcat中,有一个专门的Acceptor线程用来接受客户端连接,仅仅是用来接收,随后会将接收到的连接交给poller线程,如果了解过I/O多路复用的小伙伴因该心里都会知道,会有少量的线程来对可用连接的各种可读可写等事件进行监听,然后才会交给真正的业务线程。那这里这个poller线程就可以认为是专门用来监听事件的线程,而这个Accpetor线程仅仅只是用来接收连接。
//if we have reached max connections, wait
endpoint.countUpOrAwaitConnection();
所有的连接到这里都会进入这个方法,这个endpoint是tomcat对底层连接和协议的一个抽象封装,这个方法注释很明了,如果到达连接上线,就会阻塞住,来看一看具体实现。
protected void countUpOrAwaitConnection() throws InterruptedException {
if (maxConnections==-1) {
return;
}
LimitLatch latch = connectionLimitLatch;
if (latch!=null) {
latch.countUpOrAwait();
}
}
可以看到,如果我们的最大连接数设置为了-1,即表示不限制连接,默认是private int maxConnections = 8*1024;,然后就是获取了Limitlatch,他是在tomcat初始化的时候初始化的
if (connectionLimitLatch==null) {
connectionLimitLatch = new LimitLatch(getMaxConnections());
}
然后看看具体的限流方法countUpOrAwait实现,进入Limitlatch会发现这个类代码很少,紧接着就会出现一个很熟悉的面孔AbstractQueuedSynchronizer,没错就是juc中的那个AQS,整体实现思路其实就是对一个共享变量,这里就是这个连接数进行原子自增,如果到了阀值就等待。
private class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 1L;
public Sync() {
}
@Override
protected int tryAcquireShared(int ignored) {
long newCount = count.incrementAndGet();
if (!released && newCount > limit) {
// Limit exceeded
count.decrementAndGet();
// <0表示尝试失败
return -1;
} else {
return 1;
}
}
@Override
protected boolean tryReleaseShared(int arg) {
count.decrementAndGet();
return true;
}
}
所以tomcat的这种限流方式是很简单的,只要自己对线程同步的知识熟悉一些也很容易想到这种方式,然后了解的多一点的还会想起信号量Semaphore,他们其实是一样的功能,过需要注意的是这种限流方式只是一种参考,他只是一个对总量的限制,而且是单机限制,同时没有时间窗口的概念,如果我们在业务开发中也有限流的需求,一定要注意使用场景,是否单机,是否需要时间窗口等。