本文来自我的微信公众号 : https://mp.weixin.qq.com/s/Ldq-GsaAMLbHZ6enhwaB7A
系统开发中,可能会有这么一系列的操作,来处理数据的重复或者不对称,流程如下:
while(条件) {
//查询A
aList = getAList();
//查询B
bList = getBList();
//比对A和B
diff = check(aList, bList);
//处理diff
insert(diff);
}
因为数据量很大,所以查询getAList()操作和getBList()操作用时很长,要跑完一次所需时间较长,怎么优化?
目前的任务是单线程执行的,且getAList和getBList没有先后顺序依赖,优化方案自然而然就想到了利用多线程来并行处理。
用多线程实现并行处理
查询操作getAList和getBList的并行处理,能够大大缩短查询总共所需时间。 代码如下:
while(条件) {
//查询A
Thread T1 = new Thread(() -> {
aList = getAList();
});
T1.start();
//查询B
Thread T1 = new Thread(() -> {
bList = getBList();
});
T2.start();
//等待T1和T2执行完
T1.join();
T2.join();
//比对A和B
diff = check(aList, bList);
//处理diff
insert(diff);
}
每次执行任务,都需要创建线程,而创建是耗费时间的,那线程可不可以重复利用呢? 可以,用线程池。
用线程池优化和CountDownLatch实现线程等待
CountDownLatch主要解决一个线程(主线程)等待多个线程的场景,计算器不能循环利用,下次用的时候要再次new出来。
// 创建 2 个线程的线程池
ThreadPoolUtils {
static ExecutorService fixedThreadPool =
Executors.newFixedThreadPool(2);
public static ExecutorService getFixThreadPools(){
return fixedThreadPool;
}
}
while(条件) {
// 计数器初始化为 2
CountDownLatch latch = new CountDownLatch(2);
//查询A
ThreadPoolUtils.getFixThreadPools().execute(() -> {
aList = getAList();
latch.countDown();
});
//查询B
ThreadPoolUtils.getFixThreadPools().execute(() -> {
bList = getBList();
latch.countDown();
});
//等待两个查询执行完
latch.await();
//比对A和B
diff = check(aList, bList);
//处理diff
insert(diff);
}
仔细的你定可以看出, 查询操作 和 diff操作也是可以并行的,比如,查询完,不需要等到diff操作完成,就可以马上再去下一次查询。
这是不是像生产-消费者模型。那么设计2个队列:A队列和B队列,分别保存getAList和getBList的结果。每次A队列出一个元素,B队列出一个元素,
然后对这两个元素执行diff操作。值得注意的是:A队列和B队列都必须出一个元素,才能执行diff操作,那怎么保证同步呢?
用CyclicBarrier实现线程同步
CyclicBarrier是一组线程之间互相等待,计算器可以循环利用,一旦减到0会自动重置到初始值,且CyclicBarrier可以设置callback函数。
代码如下:
// A队列
Vector aList;
// B队列
Vector bList;
// 线程池
Executor executor = Executors.newFixedThreadPool(1);
final CyclicBarrier barrier = new CyclicBarrier(2, ()->{
executor.execute(()->check());
});
check(){
A a = aList.remove(0);
B b = bList.remove(0);
// 比对A和B
diff = check(a, b);
// 处理diff
insert(diff);
}
Thread T1 = new Thread(()->{
while(条件){
// 查询A
aList.add(getAList());
// 等待
barrier.await();
}
});
T1.start();
Thread T2 = new Thread(()->{
while(条件){
// 查询B
bList.add(getBList());
// 等待
barrier.await();
}
});
T2.start();
以上对这段逻辑的优化可以算是告一段落了?
不过,优化可以是endless的。
比如,上面是使用CountDownLatch实现了线程间的等待,其实我们也可以用Future接口来实现线程之间的等待。
while(条件) {
//查询A
Future> aList = ThreadPoolUtils.getFixThreadPools().submit(() -> getAlist());
//查询B
Future> bList = ThreadPoolUtils.getFixThreadPools().submit(() -> getBlist());
//比对A和B
diff = check(aList.get(), bList.get());
//处理diff
insert(diff);
}
或者用FutureTask工具类来实现线程之间的等待
FutureTask> aListFTask = new FutureTask<>(() -> getAlist());
ThreadPoolUtils.getFixThreadPools().submit(aListFTask);
List aList = aListFTask.get();
Future似乎解决了线程之间的等待问题,但是,它有一定的局限性,比如要实现这个场景【线程A和线程B只要有一个执行完,就执行线程C】。
虽然Future接口提供了方法isDone来检测是否已经结束,但是在复杂场景下不能写出较简洁的并发代码。
所以jdk提供了另一个CompletableFuture工具类,它提供了非常强大的Future的扩展功能,,其方法支持串行关系、并行关系、汇聚关系的多线程实现,
可以帮助我们简化异步编程的复杂性。
强大的CompletableFuture
下面给出一些代码,更直观一点。
串行关系:
CompletableFuture
CompletableFuture.supplyAsync(
() -> "Hello")
.thenApply(s -> s + " World")
.thenApply(String::toLowerCase);
System.out.println(f.join());
输出: hello world
汇聚关系(OR):
CompletableFuture
CompletableFuture.supplyAsync(()->{
int t = 2;
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
return String.valueOf(t);
});
CompletableFuture
CompletableFuture.supplyAsync(()->{
int t = 3;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
return String.valueOf(t);
});
CompletableFuture
f1.applyToEither(f2,s -> s);
System.out.println(f3.join());
输出: 3
CompletionService
future.get()有阻塞,如果有大任务,性能极低,主线程被阻塞,导致后续任务一直等待,可用completionService来解决这种阻塞,可以先执行后续,在反过来看下大任务有没有执行完,避免浪费时间。如果大任务实在太大,即使执行完其他任务,主线程还是要等待。怎么办呢?
可用Fork/Join,一个并行计算的框架,支持分治任务模型,Fork对应任务分解,Join对应结果合并。相当于单机版的MapReduce。
所以, CompletionService实现了任务先完成可优先获取。
给出一段示例代码(和CompletableFuture实现汇聚关系有点类似):
ExecutorService executor = Executors.newFixedThreadPool(2);
// 创建 CompletionService
CompletionService
// 用于保存 Future 对象
List
// 提交异步任务,并保存 future 到 futures
futures.add(cs.submit(() -> getAList()));
futures.add(cs.submit(() -> getBList()));
// 获取最快返回的任务执行结果
Integer r = 0;
try {
// 只要有一个成功返回,则 break
for (int i = 0; i < 2; ++i) {
r = cs.take().get();
// 简单地通过判空来检查是否成功返回
if (r != null) {
break;
}
}
} finally {
// 取消所有任务
for (Future
f.cancel(true);
}
// 返回结果
return r;
总结: 对于简单的并行任务,你可以通过“线程池 +Future”的方案来解决;如果任务之间有聚合关系,无论是 AND 聚合还是 OR 聚合,都可以通过 CompletableFuture 来解决;而批量的并行任务,则可以通过 CompletionService 来解决;如果并行任务中有大任务,则可以通过“Fork/Join”分治任务来解决。