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服务器下的性能,至于两者性能对比结果在不同环境下不同的原因,欢迎各位大牛一起讨论。
【本文为51CTO专栏作者“侯树成”的原创稿件,转载请通过作者微信公众号『Tomcat那些事儿』获取授权】