代码里有这样一段需求,先去取得 approval ,再去进行 deploy, 采取了多线程运行,代码运行上去之后,发现线程积压严重。请求量并不大,但是系统就hung在那里。
发现代码出现问题的大概是这段逻辑
CompletableFuture<List<Map<String, Object>>> manifestListResultFuture = CompletableFuture.supplyAsync(() -> manifestService.getManifests(cluster, ns) , forkJoinPool) .thenApply(manifestListResult -> { //do some logic });
Stream<ManifestInfo> manifestInfoStream = manifestListResultFuture.get().parallelStream().map(this::convert) .map(manifestInfo -> combineApprovals(manifestInfo,approvalCrsFuture)); if(NeedApprovalFor(paasRealm)){ manifestInfoStream = manifestInfoStream.filter( manifestInfo -> ManifestService.isApprovedFor(manifestInfo, paasRealm)); } final var targetStream = manifestInfoStream; return forkJoinPool.submit( () -> targetStream.sorted(sortByCreateTimeDesc) .map(manifestInfo -> combinePackagesMeta(manifestInfo, imageCrsFuture)) .collect(toList())).get();
这段逻辑为什么会导致线程阻塞呢? 从代码上分析这是一个典型线程池饥饿的问题。上面的代码有点复杂,咱们看一个简单的例子
public class TestStarvation { private static final Logger log = LoggerFactory.getLogger(TestStarvation.class); // return deploy result static String deploy() { return "deploying" } public static void main(String[] args) { //fixed thread ExecutorService waiterPool = Executors.newFixedThreadPool(2); waiterPool.execute(() -> { log.debug("get Approval..."); //deploy in another thread Future<String> f = waiterPool.submit(() -> { log.debug("deploy"); return deploy(); }); try { log.debug("deploy result: {}", f.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }); waiterPool.execute(() -> { log.debug("get Approval.."); Future<String> f = waiterPool.submit(() -> { log.debug("deploy"); return deploy(); }); try { log.debug("deploy result: {}", f.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }); } }
当然你运行这段代码,发现thread就会hung住, 原因是线程池就2个资源, 都被getApproval 抢占了,在方法内层的 deploy 由于和getApproval用的是同一个线程池,所以它需要等getApproval 释放资源,但是getApproval 需要等待内层的deploy 做完才能释放资源。造成了内层的线程饥饿。
当然上面是个极端例子,我们真实的case 中的原理也是一样,他们用了同一个线程池,而且他们的调用有嵌套,造成了大量的等待。
处理线程嵌套的模式都是使用不同的线程池,来保证内部和外部没有竞争。知道了原因我们再定义一个fixed thread pool executorSerice, 把它作为 manifestListResultFuture 运行的线程池,这个问题就解决了。
CompletableFuture<List<Map<String, Object>>> manifestListResultFuture = CompletableFuture.supplyAsync(() -> manifestService.getManifests(cluster, ns) , executorSerice) .thenApply(manifestListResult -> { //do some logic });