【经验分享,非教程】
这里是广告
个人接私活,2年java开发经验,中小型前后端分离web项目、python爬虫系统、桌面简单应用等。提供开发-集成-部署一条龙服务。项目可用作课题,也可用作商用。如有需要,请发送邮件到 wyxworkmail@163.com 详细咨询
最近做的Online Judge项目,在本地判题的实现过程中,遇到了一些问题,包括多线程,http通信等等。现在完整记录如下:
OJ有一个业务是:
用户在前端敲好代码,按下提交按钮发送一个判题请求给后端,后端收到这个请求后,将具体的内容再转交给一个独立的评测机服务,等待评测机给出判题的结果,再写入数据库完成一次完整的判题。
一开始,我的具体实现的思路是这样的:
首先简单介绍一下我用的评测机,是一个大佬学长写的,具体的实现和现在主流的评测机轮询数据库拿出待判题内容去判题不同,他是一个独立存在的服务。我们的后端通过Http访问到评测机的通讯模块,提交对应的代码,评测机通过docker模拟一个评测环境,跑一遍代码,比对输入输出文件,然后得到结果记录在磁盘上。同样,通过Http访问评测机得到评测的结果。(具体的实现会在后续其他博文中提到)
介绍完评测机的具体功能,然后我们就要去实现了。我先从Controller中得到用户提交请求中的代码等信息,先把这些记录存入数据库的对应表中,然后将代码等内容通过http协议发送给评测机,接着等待2s,从评测机中拿到结果。经过测试,可以实现功能。
测试功能是正确可用的,但是问题也随之而来。由于Controller执行完会返回给用户一段json消息,而等待2s是写在Contoller中的,是Contoller这个主进程的,所以执行完代码永远需要2s才能反馈给前端。这样子,用户体验差不说,逻辑也有很大的缺陷。单个用户提交可能2s就能拿到结果,如果多个提交评测机评测速度较慢,可能3s才能得到结果,后端没办法得到正确的评测机的反馈,不就全部乱套了吗?
自然而然,我就想使用多线程来解决这个问题。那么好,开始动手,由于之前接触多线程较少,所以我直接定义了一个Runnable执行一个2S读取一下评测机的方法,在Controller的线程上再开一个线程进行执行。类似这样:
new Thread(new Runnable() {
@Override
public void run() {
//这里执行两秒钟从评测机获取一次评测结果的方法
dosomething();
}
}).start();
测试是没问题的,但是新的问题出现了,我们知道Controller在spring中是单例模式存在,多线程调用的,每个用户调用它的线程是独立的,同理,一旦在Controller中执行到这个函数,一个新的线程就将开启,而不会在执行完毕后主动停止(当然也可以手动写入代码停止,但是不方便嘛),而且原生的开线程的方式是比较耗时的,那为什么不用线程池来管理呢。
为了达成进一步的修改,我在Controller中使用了如下的语句,定义了一个缓存线程池,这个线程池具体的可以参考:https://www.cnblogs.com/zhujiabin/p/5404771.html:
ExecutorService service = Executors.newCachedThreadPool();
然后在具体的实现中,使用如下语句将业务逻辑加入线程中执行:
service.execute(new Runnable() {
@Override
public void run() {//这里执行两秒钟从评测机获取一次评测结果的方法
dosomething();
}
});
经过我人工测试多次提交,完全没有卡顿,难道这就好了吗?问题没那么简单...为了知道评测机的抗压能力,我用了Postman来模拟并发,由于缓存线程池会在线程有空闲的时候使用空闲线程,没有空闲的时候开新的线程,于是我一次性发送了2000条请求给后端,于是我的8核小霸王就差点宕机了,16GB内存用了13GB,仔细一看一个tomcat容器用了3GB内存,问题就这么来了,我启动了线程池,但是没有去关闭它,哪怕我停止了这个web应用,这个线程池内的线程属于非守护进程,不会被tomcat容器干掉,所以依旧在那。
====================================================================以下是解决方案=====================================================================
为了解决这个问题,我翻了好几页百度(并没有),想通过监听web应用的开启关闭来手动关闭线程池,但是却发现了spring也实现了一个线程池类org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor,现在问题好解决了,使用spring的线程池更好配置。具体可参考:https://www.cnblogs.com/jpfss/p/9754024.html
我们可以很方便通过xml配置线程池的具体参数,作为bean存在的线程池也会在tomcat关闭后被关闭(根据常识判断,但是这点存疑,新开的线程是否会跟着web应用关闭掉,暂未测试完整),岂不美哉,下面给出配置:
在applicationContext.xml中配置bean
<!--Spring线程池-->
<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<!-- 核心线程数 -->
<property name="corePoolSize" value="5" />
<!-- 线程池维护线程的最大数量 -->
<property name="maxPoolSize" value="100" />
<!-- 允许的空闲时间, 默认60秒 -->
<property name="keepAliveSeconds" value="60" />
<!-- 缓存队列长度 -->
<property name="queueCapacity" value="50" />
<!-- 线程超过空闲时间限制,均会退出直到线程数量为0 -->
<property name="allowCoreThreadTimeOut" value="true"/>
<!-- 对拒绝task的处理策略 -->
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy" />
</property>
</bean>
在Controller中装载上
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
/**
* @author axiang
*/
@RequestMapping("/submit")
@Controller
public class SomeController {
@Autowired
private ThreadPoolTaskExecutor executor;
@RequestMapping("/dosomethingMethod")
public JsonInfo dosomethingMethod(HttpServletRequest req) {
//do something
executor.execute(new Runnable() {
@Override
public void run() {
dosomething();
}
});
return new JsonInfo();
}
}
大功告成!现在线程池交给spring维护去了,只管可劲开线程就是了:)一旦线程闲置超过配置的时长,spring就会把整个线程池回收
当然,还是存在问题的,这个线程池的实现原理还没弄懂,并且在线程中还对数据库进行了操作,暂不知道这种做法会不会对数据库的插入造成什么奇怪的影响,等待测试。