【經驗分享,非教程】
這里是廣告
個人接私活,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就會把整個線程池回收
當然,還是存在問題的,這個線程池的實現原理還沒弄懂,並且在線程中還對數據庫進行了操作,暫不知道這種做法會不會對數據庫的插入造成什么奇怪的影響,等待測試。