排查使用ExecutorService所造成數據丟失問題
我們在【Java併發編程-java.util.concurrent包中的線程池和消息隊列】這篇文章中介紹瞭如何去使用線程池進行多線程業務處理,在使用線程池的時候有很多需要注意的地方,這裏我給大家介紹一個因爲使用線程池大意造成的數據結果不符合預期的樣例。
DataQo dataQo= new DataQo();
List<DataResult> listResult = new ArrayList<>();
//多線程處理搜索請求
ExecutorService executorInsert = Executors.newFixedThreadPool(dataList.size());
CountDownLatch latch = new CountDownLatch(dataList.size());
try {
// 定義保存過程中返回的線程執行返回參數
List<Future<String>> futureList = new ArrayList<Future<String>>();
for (Data data: dataList) {
dataQo.setRelativeId(data.getId());
Future<String> futureListTop = executorInsert.submit(new CalculateQueryThread(dataQo,latch));
futureList.add(futureListTop);
}
latch.await();
// 等待線程執行完成
executorInsert.shutdown();
if(Detect.notEmpty(futureList)){
for (Future<String> future : futureList) {
if(null != future){
String listResultStr = future.get();
if(Detect.notEmpty(listResultStr)){
List<DataResult> listResultEle = (List<DataResult>) SerializableUtils.UnserializeStringToObject(listResultStr);
if(Detect.notEmpty(listResultEle)){
listResult.addAll(listResultEle);
}
}
}
}
}
}catch (Exception e) {
log.error("QueryData;error-msg:{}", e);
} finally {
//關閉線程池
if(!executorInsert.isShutdown()){
executorInsert.shutdown();
}
}
CalculateCategoryTopThread.java
/**
* 查詢總榜排行榜接口線程
* 開啓線程
* @author huzekun
*/
public class CalculateQueryThreadextends CalculateThread<List<CmsProgramCategoryRelative>>{
private static final Logger LOGGER = Logger.getLogger(CalculateCategoryTopThread.class);
private BusinessService businessService = BeanFactoryUtils.getInstance("businessService");
protected DataQo dataQo;
protected CountDownLatch latch;
@Override
public String call() throws Exception {
try {
LOGGER.debug("==========CalculateQueryThreadextends ===========");
List<DataResult> listData= businessService .listDataByQo(dataQo);
return SerializableUtils.SerializeObjectToString(listData);
} catch (Exception e) {
e.printStackTrace();
LOGGER.error("==========CalculateQueryThreadextends ==========="+e.getMessage());
}finally {
latch.countDown();
}
return null;
}
public CalculateQueryThread() {
super();
}
public CalculateQueryThread(DataQo dataQo, CountDownLatch latch) {
this.dataQo= dataQo;
this.latch = latch;
}
public CalculateQueryThread(BusinessService businessService) {
super();
this.businessService = businessService ;
}
}
這裏我們先看下這段代碼。業務很簡單,主要就是需要開啓多線程根據不同參數去請求數據之後拼接起來。就一個簡單的業務,但是有不少陷阱。上面是我們使用最初的代碼,我們直接運行最後發現只得到了三個我們需要的數據,實際上有十個我們所需的數據。之後通過
DEBUG
調試走了一遍,如期得到十個數據。這裏我們就產生一個猜想:是否因爲程序直接跑的時候遺漏了某個線程的數據查詢導致最終數據缺失。
這裏我們使用了executorInsert.isTerminated()
去循環校驗線程組是否全部執行完成以避免線程遺漏執行問題,但是這裏我們可以發現確實每個線程都是執行成功了的。
DataQo dataQo= new DataQo();
List<DataResult> listResult = new ArrayList<>();
//多線程處理搜索請求
ExecutorService executorInsert = Executors.newFixedThreadPool(dataList.size());
CountDownLatch latch = new CountDownLatch(dataList.size());
try {
// 定義保存過程中返回的線程執行返回參數
List<Future<String>> futureList = new ArrayList<Future<String>>();
for (Data data: dataList) {
dataQo.setRelativeId(data.getId());
Future<String> futureListTop = executorInsert.submit(new CalculateQueryThread(dataQo,latch));
futureList.add(futureListTop);
}
latch.await();
// 等待線程執行完成
executorInsert.shutdown();
while(true){
if(executorInsert.isTerminated()){
if(Detect.notEmpty(futureList)){
for (Future<String> future : futureList) {
if(null != future){
String listResultStr = future.get();
if(Detect.notEmpty(listResultStr)){
List<DataResult> listResultEle = (List<DataResult>) SerializableUtils.UnserializeStringToObject(listResultStr);
if(Detect.notEmpty(listResultEle)){
listResult.addAll(listResultEle);
}
}
}
}
}
break;
}
Thread.sleep(200);
}
}catch (Exception e) {
log.error("QueryData;error-msg:{}", e);
} finally {
//關閉線程池
if(!executorInsert.isShutdown()){
executorInsert.shutdown();
}
}
排除了線程遺漏執行的問題,這裏我們將排查重心轉移到了線程執行業務體
businessService .listDataByQo(dataQo)
中,我們在其中添加了日誌打印用於記錄參數以及重要執行過程信息。果不其然,這裏可以發現當前業務題執行次數正確,但是多次執行中參數確實相同的。迷霧基本已經揭開,問題出在傳遞參數上面,我們將DataQo dataQo= new DataQo()
創建參數對象放置在循環添加線程中,每個線程獨立一個新的參數對象,問題解決。
List<DataResult> listResult = new ArrayList<>();
//多線程處理搜索請求
ExecutorService executorInsert = Executors.newFixedThreadPool(dataList.size());
CountDownLatch latch = new CountDownLatch(dataList.size());
try {
// 定義保存過程中返回的線程執行返回參數
List<Future<String>> futureList = new ArrayList<Future<String>>();
for (Data data: dataList) {
DataQo dataQo= new DataQo();
dataQo.setRelativeId(data.getId());
Future<String> futureListTop = executorInsert.submit(new CalculateQueryThread(dataQo,latch));
futureList.add(futureListTop);
}
// 等待線程執行完成
executorInsert.shutdown();
if(Detect.notEmpty(futureList)){
for (Future<String> future : futureList) {
if(null != future){
String listResultStr = future.get();
if(Detect.notEmpty(listResultStr)){
List<DataResult> listResultEle = (List<DataResult>) SerializableUtils.UnserializeStringToObject(listResultStr);
if(Detect.notEmpty(listResultEle)){
listResult.addAll(listResultEle);
}
}
}
}
}
}catch (Exception e) {
log.error("QueryData;error-msg:{}", e);
} finally {
//關閉線程池
if(!executorInsert.isShutdown()){
executorInsert.shutdown();
}
}
多個線程同時執行,需要聲明不同的req參數,不可用同一個對象。不然線程不安全會造成數據共用,多個線程都使用到了同一塊內存中的數據,即每個線程使用的req最終都相等。例如上面
dataQo.setRelativeId(data.getId())
會設置[1,2,3,4,5]
,若每次沒有重新聲明會導致最後多個線程都使用了[5,5,5,5,5]
去執行線程,產生了一個數據丟失的假象。
由此可以看出,在使用多線程時不單單在技術方面要紮實,並且需要考慮比較全面,不然十分容易犯錯誤。