超时控制是我们在系统中容易忽视却又不能忽视的功能。一方面,超时并不是我们系统的主要业务功能,大多数超时控制已经被虚拟机或者服务器实现了;另一方面,超时控制是许多业务必备的隐性功能,试想,一个web应用如果在10秒默认超时过后才响应,客户能够忍受吗?
这篇文章主要总结了一些常见的超时处理机制,以及适用的场景。
首先这里提出一个实际遇到的问题,大家可以思考如何解决。在调用corba连接服务器并获取数据时需要做超时处理,以便给用户一个良好的用户体验(如service.handshake(),握手的过程就是发起corba请求的过程),可惜的是,corba生成的代码并没有提供一个超时的参数入口(查过一下,似乎在corba中连接要设置超时必须配置他的policy,但是我对此一窍不通),在这种情况下,如何用我们已知的方法来完成这个任务呢。
其实这个问题基本思路跟线程有关系,因为超时意味着阻塞,如果只有主线程,则阻塞就意味着失去了控制,基本上就谈不上超时控制了(个人认为在线程运行中间自己判断并跳出循环的方式只是线程的操作方法,并不算超时控制)。如果有主从两个线程,则在从线程阻塞过程中知道从线程超时并将其终止,则达到我们的目的。最终的思想就是将其他超时问题转化为线程超时问题。
我们来看第一种方式,即网上常见的join方式。因为没有直接的线程方法提供超时参数,则可以转换下思维,利用已经提供超时的方法join。join是Thread类的方法,意为当前线程等待调用线程结束,最重要的是,join可以传入超时控制的参数,参照如下两个原型:
join(long millis)
join(long millis, int nanos)
那么是否可以在子线程中完成待超时任务,并join子线程,当join超时便回到主线程,然后在主线程中终止子线程。HttpClient中TimeoutController就是采用这个方式。这里task是子线程,可以完成类似corba连接之类的任务,在task join之后,当前运行线程便会等待task运行结束,并且只等待timeout时间,如果timeout过了子线程仍然没有结束,则主线程可以活过来并中断子线程。
public static void execute(Thread task, long timeout) throws TimeoutException {
task.start();
try {
task.join(timeout);
} catch (InterruptedException e) {
/* if somebody interrupts us he knows
what he is doing */
}
if (task.isAlive()) {
task.interrupt();
throw new TimeoutException();
}
}
第一种方式没有那么直观,相对来说,join有些绕弯,有没有更直接点的方式?那就是用concurrent包的Future,future在得到计算结果时有个超时参数,利用此方式,可以有效的实现超时
ExecutorService executor = Executors.newSingleThreadExecutor();
FutureTask<LicenseService> future = new FutureTask<LicenseService>(
new Callable<LicenseService>() {
public LicenseService call() {
LicenseServerAdapter adapter = new LicenseServerAdapter(
info);
LicenseService service = adapter.getLicenseService();
service.handShake();
return service;
}
});
executor.execute(future);
return future.get(timeout * 1000, TimeUnit.MILLISECONDS);
说到超时,还有另外一个超时的场景,即web服务器的session超时。可以想象一下,如果自己实现了web服务器,需要实现session的功能,这时该如何实现session的超时呢?这个问题可以有很多方式解决,这里提供两个,第一是在每个session对象中存入一个时间戳,每次访问时先检查是否已经过期,如过期,则回收该session,如没有过期,则更新该时间戳,但是有个缺点,如果在过期后都不再次访问,则session永远不会被回收,当然,这里还是可以改进的,比如每次访问时检查所有session是否过期,又会带来效率的问题,有兴趣的朋友可以思考一下。另外一个方式是tomcat的实现,即一个后台线程定时对session做扫描,在扫描过程中实施session的时间戳更新或回收问题,参看代码
public void backgroundProcess() {
count = (count + 1) % processExpiresFrequency;
if (count == 0)
processExpires();
}
/**
* Invalidate all sessions that have expired.
*/
public void processExpires() {
long timeNow = System.currentTimeMillis();
Session sessions[] = findSessions();
int expireHere = 0 ;
if(log.isDebugEnabled())
log.debug("Start expire sessions " + getName() + " at " + timeNow + " sessioncount " + sessions.length);
for (int i = 0; i < sessions.length; i++) {
if (sessions[i]!=null && !sessions[i].isValid()) {
expireHere++;
}
}
long timeEnd = System.currentTimeMillis();
if(log.isDebugEnabled())
log.debug("End expire sessions " + getName() + " processingTime " + (timeEnd - timeNow) + " expired sessions: " + expireHere);
processingTime += ( timeEnd - timeNow );
}
最后一个关于超时的话题也很有意思,在linux中c函数的api的connect方法是没有提供超时参数的,如下:
int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
那么java是如何通过native方法的包装提供出带参数的connect api呢?其实这里思路与之前是一致的,即寻找一个替代的有超时参数的方法。首先是设置connect为非阻塞,然后调用connect并立即返回,最后将写事件注册到带超时的poll。
/*
* To do a timed connect we make the socket non-blocking
* and poll with a timeout;
*/
if (attachTimeout > 0) {
dbgsysConfigureBlocking(socketFD, JNI_FALSE);
}
err = dbgsysConnect(socketFD, (struct sockaddr *)&sa, sizeof(sa));
if (err == DBG_EINPROGRESS && attachTimeout > 0) {
err = dbgsysFinishConnect(socketFD, (long)attachTimeout);
if (err == DBG_ETIMEOUT) {
dbgsysConfigureBlocking(socketFD, JNI_TRUE);
RETURN_ERROR(JDWPTRANSPORT_ERROR_TIMEOUT, "connect timed out");
}
}
int dbgsysFinishConnect(int fd, long timeout) {
int rv = dbgsysPoll(fd, 0, 1, timeout);
if (rv == 0) {
return DBG_ETIMEOUT;
}
if (rv > 0) {
return 0;
}
return rv;
}
int dbgsysPoll(int fd, jboolean rd, jboolean wr, long timeout) {
struct pollfd fds[1];
int rv;
fds[0].fd = fd;
fds[0].events = 0;
if (rd) {
fds[0].events |= POLLIN;
}
if (wr) {
fds[0].events |= POLLOUT;
}
fds[0].revents = 0;
rv = poll(&fds[0], 1, timeout);
if (rv >= 0) {
rv = 0;
if (fds[0].revents & POLLIN) {
rv |= DBG_POLLIN;
}
if (fds[0].revents & POLLOUT) {
rv |= DBG_POLLOUT;
}
}
return rv;
}
参考文章:
http://blog.donews.com/maverick/archive/2005/08/10/502286.aspx
http://my.oschina.net/astute/blog/92339