您还有心跳吗?超时机制分析
问题描述
在 C/S(Client/Server)架构中,为了减少频繁建立连接的开销,我们有时会长时间保持一个连接。但同时,通常会设置一个超时时间:若在该时间内连接未发起任何请求,则将其断开,以减少服务端负载并节约资源。
该机制一般建议在服务端实现。这是因为当客户端强制关闭或意外断开连接时,服务端往往无法立即感知。若将超时机制完全交由客户端实现,在上述异常情况下,该机制可能会失效。
这一问题看似普通,往往容易被忽视。但最近在项目中观察到一种该机制的糟糕实现,故在此深入分析其潜在问题及优化方案。
问题分析及解决方案
服务端通常需要维护大量连接,因此普遍做法是创建一个定时器,定期检查所有连接中哪些已超时。此外,关键在于:当收到客户端发来的数据时,如何高效地刷新该连接的超时信息?
初始实现与可见性问题
最近看到的一种实现方式如下:
public class Connection {
private long lastTime;
public void refresh() {
lastTime = System.currentTimeMillis();
}
public long getLastTime() {
return lastTime;
}
// ......
}在每次收到客户端数据时,调用 refresh 方法更新最后活动时间。然后在定时器任务中,通过比较当前时间与每个连接的 getLastTime() 来判定是否超时:
public class TimeoutTask extends TimerTask {
public void run() {
long now = System.currentTimeMillis();
for (Connection c : connections) {
if (now - c.getLastTime() > TIMEOUT_THRESHOLD) {
// timeout, do something
}
}
}
}熟悉并发编程的读者可能已经发现了问题:内存可见性。调用 refresh 方法的线程(通常是 IO 线程)与执行定时器的线程并非同一个线程。若 lastTime 未做同步处理,定时器线程读到的可能是旧值,导致将活跃连接误判为超时并断开。
性能考量与测试
有读者可能会想到使用 volatile 修饰 lastTime。这确实解决了可见性问题,但作为服务端,高性能往往是核心诉求。volatile 写操作以及频繁调用 System.currentTimeMillis() 是否存在性能瓶颈?
以下是我本地环境的一组测试数据,代码供参考:
public class PerformanceTest {
private static long i;
private volatile static long vt;
private static final int TEST_SIZE = 10000000;
public static void main(String[] args) {
long time = System.nanoTime();
for (int n = 0; n < TEST_SIZE; n++)
vt = System.currentTimeMillis();
System.out.println("volatile 写 + 取系统时间:" + (-time + (time = System.nanoTime())));
for (int n = 0; n < TEST_SIZE; n++)
i = System.currentTimeMillis();
System.out.println("普通写 + 取系统时间:" + (-time + (time = System.nanoTime())));
for (int n = 0; n < TEST_SIZE; n++)
synchronized (PerformanceTest.class) { }
System.out.println("空的同步块(synchronized):" + (-time + (time = System.nanoTime())));
for (int n = 0; n < TEST_SIZE; n++)
vt++;
System.out.println("volatile 变量自增:" + (-time + (time = System.nanoTime())));
for (int n = 0; n < TEST_SIZE; n++)
vt = i;
System.out.println("volatile 写:" + (-time + (time = System.nanoTime())));
for (int n = 0; n < TEST_SIZE; n++)
i = vt;
System.out.println("volatile 读:" + (-time + (time = System.nanoTime())));
for (int n = 0; n < TEST_SIZE; n++)
i++;
System.out.println("普通自增:" + (-time + (time = System.nanoTime())));
for (int n = 0; n < TEST_SIZE; n++)
i = n;
System.out.println("普通读写:" + (-time + (time = System.nanoTime())));
}
}测试一千万次,结果如下(耗时单位:纳秒,包含循环本身的时间):
- 238,932,949 : volatile 写 + 取系统时间
- 144,317,590 : 普通写 + 取系统时间
- 135,596,135 : 空的同步块(synchronized)
- 80,042,382 : volatile 变量自增
- 15,875,140 : volatile 写
- 6,548,994 : volatile 读
- 2,722,555 : 普通自增
- 2,949,571 : 普通读写
从数据可以看出,volatile 写操作配合获取系统时间的耗时非常高,甚至接近一次无竞争的同步块操作。获取系统时间本身也是一项相对耗时的操作。在高负载场景下,若每次刷新都执行此操作,成本过高。
优化方案
针对上述问题,优化思路如下:
- 同步问题:跨线程数据操作必须保证可见性。
- 减少耗时操作:能否不在每次刷新时都获取系统时间?因为刷新调用在高负载下非常频繁。
如果不直接在刷新时获取时间,该如何判定超时?我的方案是:将时间的掌控交给定时器,连接对象仅维护状态标志。
具体实现如下:为每个连接维护一个计数器,并在连接对象中设置一个 volatile boolean 变量 reset。
- 刷新时:仅设置
reset = true(成本极低)。 - 定时检查时:若
reset为true,则计数器归零并将reset复位;否则计数器加一。若计数器超过阈值,则判定超时。
由于计数器仅由定时器线程维护,无需额外的同步处理。从测试数据来看,普通变量的操作成本极低。
public class Connection {
int count = 0;
volatile boolean reset = false;
public void refresh() {
// 仅在需要时写入,减少 volatile 写操作
if (!reset) {
reset = true;
}
}
}public class TimeoutTask extends TimerTask {
public void run() {
for (Connection c : connections) {
if (c.reset) {
c.reset = false;
c.count = 0;
} else if (++c.count >= TIMEOUT_COUNT) {
// timeout, do something
}
}
}
}代码中的 TIMEOUT_COUNT 等于超时时间除以定时器的周期。周期大小既影响定时器的执行频率,也会影响实际超时时间的波动范围(这种波动在第一种方案中同样存在,难以完全避免,且通常不需要毫秒级的精确度)。
方案分析
- 可见性保证:
reset变量加上了volatile修饰,保证了多线程操作的可见性。虽然两个线程都可能对该变量进行写操作,但无论线程如何穿插执行,都不会影响逻辑的正确性。 性能优化:在
refresh方法中,我增加了一个条件判断if (!reset)。这看似多了一次volatile读操作,实则有益。- 在高负载下,
refresh会被频繁调用,意味着reset长时间处于true状态。 - 加上条件后,大部分情况下只会执行读操作,不会执行写操作。
- 从测试数据来看,
volatile读操作的性能显著优于写操作。 - 仅在
reset为false时(即定时器的一个周期内最多一次)才会执行写操作。这对高负载情况下的性能优化更有意义。
- 在高负载下,
综上所述,该方案在保证了线程安全的前提下,显著降低了高频调用下的性能开销。
说明:本文示例代码基于 Java 传统 Timer 机制编写。在实际生产环境中,建议优先使用 ScheduledExecutorService 替代 Timer,以获得更好的线程管理和异常处理能力。此外,涉及耗时统计时,System.nanoTime() 通常比 System.currentTimeMillis() 更适合用于测量时间间隔。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/nin-hai-you-xin-tiao-ma--chao-shi-ji-zhi-fen-xi.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。