本文的作者是我之前一起从事应用服务器研发的同事杨高强,高强现就职于某互联网巨头,功底深厚,技术牛X。以下为正文部分
Tomcat的LimitLatch类用于控制网络通信的socket接收上限,在Tomcat7时引入,实现简单,借此可以学习一下线程同步的相关知识。
LimitLatch依赖内部类Sync进行线程同步,而Sync继承自大家熟悉的AbstractQueuedSynchronizer。AQS是java.util.concurrent的核心组件,诸多常用的线程同步工具类都能够找到他的影子,读者可以翻阅ReentrantLock、CountDownLatch、Semaphore等类的源码。
//if we have reached max connections, wait
countUpOrAwaitConnection();
SocketChannel socket = null;
try {
// Accept the next incoming connection from the server socket
socket = serverSock.accept();
……
不管是NIO还是BIO,Tomcat在接收socket前,都要通过countUpOrAwaitConnection方法获取资源,如果已经达到最大连接数,则需要当前线程等待资源释放。该方法最终会调用到LimitLatch的内部类Sync的acquireSharedInterruptibly方法,即AQS的acquireSharedInterruptibly方法。
从内部类Sync的重载方法我们能看到Sync是一个共享模式的同步器,重载了tryAcquireShared和tryReleaseShared两个方法,而两个方法之所以能够如此简单,就是因为父类AQS在背后默默完成了其他所有的排队、等待、激活等一系列逻辑。
protected int tryAcquireShared(int ignored) {
long newCount = count.incrementAndGet();
if (!released && newCount > limit) {//自增后没有超过资源上限则获取成功 // Limit exceeded
count.decrementAndGet();//资源获取失败,回退
return -1;
} else {
return 1;
}
}
在获取共享资源时,LimitLatch.Sync使用了原子变量AtomicLong,利用其自增的CAS原子操作结果与设定的共享资源数量上限进行比较,如果超出上限则目前无法获取资源,由AQS放入等待队列等待下次触发。LimitLatch中定义了released属性,该属性为true时,无论如何都会获取到共享资源。
public boolean releaseAll() {
released = true;//标志位置为ture后,后续均可获取资源
return sync.releaseShared(0);//通知等待线程重新获取资源
}
这里就有一个问题了,既然无论如何都会获取到资源,LimitLatch就没有存在的必要,那为何还要这样一个看似多余的released 属性呢?这里其实考虑到一个状态变更的问题,当由一个LimitLatch控制资源获取量变更为无需LimitLatch时,仅仅将LimitLatch置为null从而跳过资源竞争是不够的。
如果之前存在在等待队列中等待资源的线程,而此时没有资源释放,那么在状态变更后线程仍然会处于等待状态,这与“无限制”的状态是不符的,此时需要将released属性置为true,然后通过一次资源释放由AQS触发所有等待线程重新获取资源,这个时候所有线程均会获取资源立即返回。
protected boolean tryReleaseShared(int arg) {
count.decrementAndGet();//自减释放资源
return true;
}
资源释放时的代码就更简单了,直接将代表资源的原子变量AtomicLong自减从而释放资源就完成了。而后续的唤醒等待资源的线程等工作已经由AQS代劳了。
写到这里,问题又来了,这个功能完全可以由JDK自带的Semaphore类来完成啊。如果非要再写一个那一定是因为性能的原因了,毕竟该类要使用在接收Socket的前面,对性能有直接影响。下面代码为Semaphore类(JDK1.8)的FairSync重写的tryAcquireShared方法,本质上与LimitLatch并无什么不同,都是CAS自旋:
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 || compareAndSetState(available, remaining))
return remaining;
}
}
话不多说,开始性能测试,测试场景分为64线程竞争64个资源以及64线程竞争32个资源,循环300w次。测试结果竟然是LimitLatch性能要比Semaphore性能低近10%左右,这个。。。一定是我打开的方式不对。
我们回顾一下,当前使用的环境为Windows,JDK版本为1.8,而tomcat7引入LimitLatch的时候正是JDK1.6横行的时代。笔者将JDK切换到1.6进行测试,果然结果变为两者不相上下;而后将测试代码上传到了服务器,在Linux、JDK1.6环境下重新测试,结果LimitLatch扭转乾坤,反超Semaphore性能20%左右,切换到JDK1.8性能领先Semaphore约40%!很明显Tomcat更注重的是Linux服务器下的性能,至于两者性能对比结果在不同环境下不同的原因,欢迎各位大牛一起讨论。
讲到这里,读者是不是对AQS的源码也非常感兴趣了呢,赶紧读起来吧?
☆★☆更多精彩内容☆★☆****
一台机器上安装多个Tomcat 的原理(回复001)
监控Tomcat中的各种数据 (回复002)
启动Tomcat的安全机制(回复003)
乱码问题的原理及解决方式(回复007)
Tomcat 日志工作原理及配置(回复011)
web.xml 解析实现(回复 012)
线程池的原理( 回复 014)
Tomcat 的集群搭建原理与实现 (回复 015)
类加载器的原理 (回复 016)
类找不到等问题 (回复 017)
代码的热替换实现(回复 018)
Tomcat 进程自动退出问题 (回复 019)
为什么总是返回404? (回复 020)
...
iOS赞赏码!
PS: 对于一些 Tomcat常见问题,在公众号的【常见问题】菜单中,有需要的朋友欢迎关注查看。
觉得本文对你有帮助?请分享给更多人支持一下吧,谢谢****
关注『 Tomcat那些事儿 』 ,发现更多精彩文章!了解各种常见问题背后的原理与答案。深入源码,分析细节,内容原创,欢迎关注。
本文分享自微信公众号 - Tomcat那些事儿(tomcat0000)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。