一、项目背景
这几天要开发一个类似支付宝那种年度账单统计的功能,就是到元旦后支付完会把用户这一年的消费情况从各个维度(我们把这一个维度称作一个指标)统计分析形成一张报告展示给用户。
这个功能实现用到了CountDownLatch。假如统计分析用户的年底消费账单是10个指标。则希望用10线程并发去分别统计这10个指标,等10个线程都完成计算后,最后在通过另外一个线程汇总10个指标返给前端展示给用户。
二、问题描述
其中出现了这样一个问题,生成第一个用户的年度账单是10个指标计算完后,最后一个线程进行最后的结果统计。这没问题。但是在生成第二个用户年底账单时,返给前端的是空。但是数据库里却生成了第二用户的年度账单。后面生成的所有用户年度账单都是空,且数据库都有每个用户的账单。
三、错误代码示例
package com.lsl.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;
@Controller
@RequestMapping("/latch")
public class CountDownLatchController {
//创建固定线程池最大线程数10
private static ExecutorService executor = Executors.newFixedThreadPool(10);
//模拟并发任务数
private static int taskNum = 10;
//计数器
CountDownLatch latch = new CountDownLatch(taskNum);
@PostMapping(value = "execTask", produces = "application/json;charset=UTF-8")
@ResponseBody
public String execTask(){
for (int i = taskNum;i>=1;i--){
String name = "thread";
Future<Map> submit = executor.submit(new CountNumTask(latch,name, i));
// try {
// Map map = submit.get();
// String ThreadName = map.get("name").toString();
// String total = map.get("total").toString();
// System.err.println("ThreadName:" + ThreadName + ",total=" + total);
// } catch (InterruptedException e) {
// e.printStackTrace();
// } catch (ExecutionException e) {
// e.printStackTrace();
// }
}
try {
latch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
//正常情况下,等10个任务执行完毕下面的主线程才输出
System.out.println("主线程开始执行了.....");
return "success";
}
/**
* 线程任务
*/
private class CountNumTask implements Callable<Map>{
private String name;
private int num;
private CountDownLatch latch;
public CountNumTask(CountDownLatch latch,String name,int num){
this.latch = latch;
this.name = name;
this.num = num;
}
@Override
public Map call() throws Exception {
long st = new Date().getTime();
Map resultMap = new HashMap();
String threadName = name + num;
resultMap.put("name",threadName);
int total = 0;
for (int i =0;i<=num;i++){
total += i;
}
Thread.sleep(num+100);//每个任务sleep不同,模拟线程耗时不一样
resultMap.put("total",total);
latch.countDown();
long ed = new Date().getTime();
System.err.println("ThreadName:" + threadName + ",total=" + total + ",耗时=" + (ed-st));
return resultMap;
}
}
}
第一次调用截图:
第二次调用截图:
从上面截图可以看出,10个指标线程还没有运行完,主线程就先输出了。
四、原因分析
原来是CountDownLatch latch = new CountDownLatch(taskNum);定义成成员变量了。这个应用定义成局部变量,也就是放在方法内。
原因是spring托管的bean都是单例的,第一次调用结束后latch.getCount()已经是0了,然后后面的调用就不会等待前面子任务完成就开始执行主线程任务了。这就是为什么数据库里有每次的数据,而没有返给前端的原因。
网上有的说法是错误:他们认为是线程内的latch.countDown();没有执行,应该把这个放在fianlly语句快内,保证改计数器减1操作每次都能执行。
如果是这样那么计数器没有到0,如果在方法内latch.await(10, TimeUnit.SECONDS);这个语句就可以看出,10秒钟后主线程也会执行,那么上面的10个线程如果每个任务的耗时都超过10秒才能出现主线程比子任务输出早的情况。如果采用的是latch.await();那么主线程就会被永远阻塞了,因为计数器没有到0。这个前提是CountDownLatch latch = new CountDownLatch(taskNum)这个定义的是局部变量。
五、正确的代码示例
package com.lsl.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;
@Controller
@RequestMapping("/latch")
public class CountDownLatchController {
//创建固定线程池最大线程数10
private static ExecutorService executor = Executors.newFixedThreadPool(10);
//模拟并发任务数
private static int taskNum = 10;
@PostMapping(value = "execTask", produces = "application/json;charset=UTF-8")
@ResponseBody
public String execTask(){
//计数器
CountDownLatch latch = new CountDownLatch(taskNum);
for (int i = taskNum;i>=1;i--){
String name = "thread";
Future<Map> submit = executor.submit(new CountNumTask(latch,name, i));
// try {
// Map map = submit.get();
// String ThreadName = map.get("name").toString();
// String total = map.get("total").toString();
// System.err.println("ThreadName:" + ThreadName + ",total=" + total);
// } catch (InterruptedException e) {
// e.printStackTrace();
// } catch (ExecutionException e) {
// e.printStackTrace();
// }
}
try {
latch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
//正常情况下,等10个任务执行完毕下面的主线程才输出
System.out.println("主线程开始执行了.....");
return "success";
}
/**
* 线程任务
*/
private class CountNumTask implements Callable<Map>{
private String name;
private int num;
private CountDownLatch latch;
public CountNumTask(CountDownLatch latch,String name,int num){
this.latch = latch;
this.name = name;
this.num = num;
}
@Override
public Map call() throws Exception {
long st = new Date().getTime();
Map resultMap = new HashMap();
String threadName = name + num;
resultMap.put("name",threadName);
int total = 0;
for (int i =0;i<=num;i++){
total += i;
}
Thread.sleep(num+100);//每个任务sleep不同,模拟线程耗时不一样
resultMap.put("total",total);
// if (num!=5)
latch.countDown();
long ed = new Date().getTime();
System.err.println("ThreadName:" + threadName + ",total=" + total + ",耗时=" + (ed-st));
return resultMap;
}
}
}
两次调用截图:
六、其他几点细节
细节1、从代码分析,thread10是第一进入进入线程的,为什么确实最后进入线程的thread1先输出了呢?原因从截图中我打印的耗时就能看出来,就是thread10耗时最长,所以最晚输出。
细节2、如果我把下图的代码放开,且把计数器还定义成成员变量,会有什么结果呢?(结果可能出乎大家意料,很好玩哦)
我把代码附下面:
package com.lsl.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;
@Controller
@RequestMapping("/latch")
public class CountDownLatchController {
//创建固定线程池最大线程数10
private static ExecutorService executor = Executors.newFixedThreadPool(10);
//模拟并发任务数
private static int taskNum = 10;
//计数器
CountDownLatch latch = new CountDownLatch(taskNum);
@PostMapping(value = "execTask", produces = "application/json;charset=UTF-8")
@ResponseBody
public String execTask(){
for (int i = taskNum;i>=1;i--){
String name = "thread";
Future<Map> submit = executor.submit(new CountNumTask(latch,name, i));
try {
Map map = submit.get();
String ThreadName = map.get("name").toString();
String total = map.get("total").toString();
System.err.println("ThreadName:" + ThreadName + ",total=" + total);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
try {
latch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
//正常情况下,等10个任务执行完毕下面的主线程才输出
System.out.println("主线程开始执行了.....");
return "success";
}
/**
* 线程任务
*/
private class CountNumTask implements Callable<Map>{
private String name;
private int num;
private CountDownLatch latch;
public CountNumTask(CountDownLatch latch,String name,int num){
this.latch = latch;
this.name = name;
this.num = num;
}
@Override
public Map call() throws Exception {
long st = new Date().getTime();
Map resultMap = new HashMap();
String threadName = name + num;
resultMap.put("name",threadName);
int total = 0;
for (int i =0;i<=num;i++){
total += i;
}
Thread.sleep(num+100);//每个任务sleep不同,模拟线程耗时不一样
resultMap.put("total",total);
// if (num!=5)
latch.countDown();
long ed = new Date().getTime();
System.err.println("ThreadName:" + threadName + ",total=" + total + ",耗时=" + (ed-st));
return resultMap;
}
}
}
运行截图如下图:
从上面截图是不是发现了很奇怪啊!计数器定义成了成员变量,第二次调用为什么主线程是等前面10子任务都完成了才输出呢?而且子任务的输出顺序也对了,是从thread10到thread1依次输出,虽然thread10耗时最长,也是第一个输出了!!!
出现上述2个反常,大家知道什么原因吗?欢迎在评论区留言!!!
具体原因我会过几天在公布吧!!!!
【因为submit.get()方法会依次获取线程的结果。而不是先获取到最新执行完的线程结果】
细节3、如果把线程内的latch.countDown()位置调整到最开始位置,会出现什么结果呢?
如下图:
运行结果截图如下:
从细节3的现象可以看出,latch.countDown()位置放到线程任务的最后面,这个很重要。因为在latch.wait()实时读取计数器的数值是否到0了,一旦到0了,后面的主线程就里面执行了。这就和另外的CyclicBarrier(循环栅栏)有所区别了。