前言
日常搬砖,接到了将串行调用RPC服务(不要问我为啥串行 我也不明白)的业务逻辑优化的活儿,经过苦心思考,问了周边大神同学,醍醐灌顶,有了此文。
多线程获取计算结果方式
一想到多线程查询数据再汇总,我想到的是以下三种方式,根据业务及性能选择最优解。
Future同步式
- Future同步式,想必大家都很清楚,就是
get
方法,源码注释如下。
Waits if necessary for the computation to complete, and then retrieves its result. 释义:Future.get()在线程计算结束前,一直处于等待状态。
代码
public static void main(String[] args) throws InterruptedException, ExecutionException {
long l = System.currentTimeMillis();
ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<Integer> future = executorService.submit(() -> {
System.out.println("执行耗时操作...");
timeConsumeOp();
// 耗时3000ms
return 100;
});
Future<Integer> future1 = executorService.submit(() -> {
System.out.println("执行耗时操作1...");
// 耗时2000ms
timeConsumeOp1();
return 101;
});
// 依次将future、future1添加进列表
List<Future<Integer>> futureList = Lists.newArrayList(future, future1);
int s = 0;
for (Future<Integer> future2 : futureList) {
// 线程计算结果
System.out.println("返回结果:" + future2.get());
s = s + future2.get();
}
// 求和
System.out.println("计算结果:" + s);
// 主线程耗时
System.out.println("主线程耗时:" + (System.currentTimeMillis() - l));
}
private static void timeConsumeOp() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void timeConsumeOp1() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
复制代码
- 结果
执行耗时操作...
执行耗时操作1...
返回结果:100
返回结果:101
计算结果:201
主线程耗时:3077
复制代码
解析
-
调用是同步的,从主线程耗时可以看出,主线程处于等待状态,等待计算结果返回才开始运行,因此耗时为3000ms左右。
-
获取计算结果取决于调用顺序,尽管
future1
计算时间比future
短(2000ms < 3000ms),应该是先得到101再得到100,但是Future.get()
方法取决于调用顺序,因为遍历futureList
时先调用的future
的get
方法。 -
我们将
futureList
元素顺序换下,看看结果。
// 先future1
List<Future<Integer>> futureList = Lists.newArrayList(future1, future);
复制代码
- 结果
执行耗时操作...
执行耗时操作1...
返回结果:101
返回结果:100
计算结果:201
主线程耗时:3076
复制代码
CompletionService
CompletionService
使用excutor
实现任务执行,添加了LinkedBlockingQueue
将完成的FutureTask
添加进来。
// 使用线程数为2的线程池初始化
private static CompletionService<Integer> completionService = new ExecutorCompletionService<> (Executors.newFixedThreadPool(2));
复制代码
代码
public static void main(String[] args) throws InterruptedException, ExecutionException {
long l = System.currentTimeMillis();
Future<Integer> future = completionService.submit(() -> {
System.out.println("执行耗时操作...");
timeConsumeOp();
return 100;
});
Future<Integer> future1 = completionService.submit(() -> {
System.out.println("执行耗时操作1...");
timeConsumeOp1();
return 101;
});
int s = 0;
for (int i = 0; i < 2; i++) {
int result = completionService.take().get();
System.out.println("返回结果:" + result);
s += result;
}
System.out.println("计算结果:" + s);
System.out.println("主线程耗时:" + (System.currentTimeMillis() - l));
}
private static void timeConsumeOp() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void timeConsumeOp1() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
复制代码
- 结果
执行耗时操作...
执行耗时操作1...
返回结果:101
返回结果:100
计算结果:201
主线程耗时:3066
复制代码
解析
- 调用是同步的,从主线程耗时可以看出,主线程处于等待状态,等待计算结果返回才开始运行,因此耗时为3000ms左右。
- 获取计算结果取决于计算完成顺序,尽管
future1
计算时间比future
短(2000ms < 3000ms),因此得到101再得到100。
Future回调式 + CountDownLatch
Future回调式
- 实现Future回调式的方式有多种,
Guava
就提供了两种方式,ListeningExecutorService
、CompletableFuture
,也可以使用Netty
的Future
。 - 理解回调式,Android开发的经验就有用武之地了,其实就是异步调用,像Android里主线程负责渲染,访问网络的线程得到结果了回调触发主线程修改UI。
- 本文使用
CompletableFuture
实现,有兴趣的可以使用其他方式。
CountDownLatch(CycleBarrier)
- CountDownLatch类似于“计数器”,
countdown()
方法会将“计数器”减一,调用的await()
的线程会在“计数器”为0之前,处于等待状态。
结果存储
- 回调式的特点在于计算结果是在工作线程中返回的,因此要将结果同步下,可以使用
concurrent
集合,或者使用类似Map<AtomicInteger,Object>
数据结果将各个子线程的结果汇总下。
代码
public static void main(String[] args) throws Exception{
CountDownLatch countDownLatch = new CountDownLatch(2);
List<Integer> list = Collections.synchronizedList(new ArrayList<>(2));
long l = System.currentTimeMillis();
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println("执行耗时操作...");
timeConsumeOp();
return 100;
});
CompletableFuture<Integer> completableFuture1 = CompletableFuture.supplyAsync(new Supplier<Integer>() {
@Override
public Integer get() {
System.out.println("执行耗时操作1...");
timeConsumeOp1();
return 101;
}
});
completableFuture.whenComplete((integer, throwable) -> {
if (throwable != null) {
System.out.println("运行错误");
} else {
System.out.println("计算结果:" + integer + " 线程名:" + Thread.currentThread().getName());
list.add(integer);
}
countDownLatch.countDown();
});
completableFuture1.whenComplete((integer, throwable) -> {
if (throwable != null) {
System.out.println("运行错误");
} else {
System.out.println("计算结果:" + integer + " 线程名:" + Thread.currentThread().getName());
list.add(integer);
}
countDownLatch.countDown();
});
System.out.println("计算结果汇总前 主线程还在运行:" + (System.currentTimeMillis() - l));
// 主线程等待
countDownLatch.await();
int s = 0;
for (int i : list) {
s += i;
}
System.out.println("计算结果为:" + s + " 耗时 " + (System.currentTimeMillis() - l));
}
复制代码
- 结果
执行耗时操作...
执行耗时操作1...
计算结果汇总前 主线程还在运行:70
计算结果:101 线程名:ForkJoinPool.commonPool-worker-2
计算结果:100 线程名:ForkJoinPool.commonPool-worker-1
计算结果为:201 耗时 3072
复制代码
解析
- 回调式,即异步调用不会使主线程处于等待状态。
countDownLatch.countDown()
的时机要在计算结果存储之后,否则可能会漏掉线程运行结果。countDownLatch.await()
主线程会等待所有的线程countDown
后,开始运行,可以看到耗时。- 使用了
Collections.synchronizedList
,原因为结果在不同线程返回,加锁保证计算结果存储。
结语
本文综合了几种笔者能想到的方案,并进行了简要分析,不见得都对,大家有更好的方案,可以与笔者交流。