排查使用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]
去执行线程,产生了一个数据丢失的假象。
由此可以看出,在使用多线程时不单单在技术方面要扎实,并且需要考虑比较全面,不然十分容易犯错误。