在分布式系统或者微服务系统中,我们经常会调用别的服务接口,但返回的时间却不可以预料的,如果我们等待,在高并发场景中,分分钟造成严重阻塞,这时候我们需要多线程来避免这个问题,下面我们来看看例子:
准备:getPrice(Shring shop) , getDiscount(String shop) , getRate(String shop) 每个服务模拟延时,这里为了展示效果,设置为服务返回需要1秒时间
场景1 :我们需要获取同一商品,各个商店的报价 方式一:使用并行流 public static void main(String[] args) throws InterruptedException { List shops=new ArrayList<>(); //初始化6个商店 shops.add("商店1-"); shops.add("商店2-"); shops.add("商店3-"); shops.add("商店4-"); shops.add("商店5-"); shops.add("商店6-"); long startTime = System.currentTimeMillis(); TestCompletable1(shops); System.out.println("耗时:"+(System.currentTimeMillis()-startTime)+"ms"); } private static List TestCompletable1(List shops){ return shops.stream().parallel().map(shop -> getPrice(shop)) .collect(Collectors.toList()); } //CPU核数=6 输出:耗时:1042ms
如果使用并行流异步操作,对于6核CUP开6个线程没毛病,下面我们来看看如果商店数量超过CUP核数,
public static void main(String[] args) throws InterruptedException { List shops=new ArrayList<>(); //初始化6个商品 shops.add("商店1-"); shops.add("商店2-"); shops.add("商店3-"); shops.add("商店4-"); shops.add("商店5-"); shops.add("商店6-"); shops.add("商店7-"); long startTime = System.currentTimeMillis(); TestCompletable1(shops); System.out.println("耗时:"+(System.currentTimeMillis()-startTime)+"ms"); } private static List TestCompletable1(List shops){ return shops.stream().parallel().map(shop -> getPrice(shop)) .collect(Collectors.toList()); } //CPU核数=6 输出:耗时:2043ms
我们可以看到,耗时多了一倍,这是为什么呢?因为并行流底层采用的是forkJoinPool,连接池默认是CPU核数,当6个商品开6个线程查询价格,6个线程可以同时执行,当需要查询第7个商店,那么同时只能执行6个线程,剩下一个线程在等待,当1秒后执行完了6个商店查询,还要花多1秒查询剩下的商店价格,也就是说,如果商店数量是1到6个,需要1秒多,7到12个,需要2秒多……
小编对于这种场景,不喜欢用并行流,不灵活
方式二 :CompletableFuture
public static void main(String[] args) throws InterruptedException { List shops=new ArrayList<>(); //初始化6个商品 shops.add("商店1-"); shops.add("商店2-"); shops.add("商店3-"); shops.add("商店4-"); shops.add("商店5-"); shops.add("商店6-"); shops.add("商店7-"); long startTime = System.currentTimeMillis(); TestCompletable2(shops); System.out.println("耗时:"+(System.currentTimeMillis()-startTime)+"ms"); } private static void TestCompletable2(List shops) { Executor executor = Executors.newFixedThreadPool(shops.size()); List> collect = shops.stream() .map(shop -> CompletableFuture.supplyAsync(() -> getPrice(shop), executor)) .collect(Collectors.toList()); System.out.println(collect.stream().map(CompletableFuture::join).collect(Collectors.toList())); } //CPU核数=6 输出:6个商店,耗时:1048ms 7个商店,耗时:1045ms
在JAVA8后,对于对线程编程多了一个CompletableFuture,它实现了原来的Future接口,大大提高了开发效率,并且结合线程数,绝配。这里线程池初始化线程数量是商店数量,是保证每个商店都有子线程去执行,默认使用的线程池也是ForkJoinPool,这个时候需要我们自定义线程池。注意的是,不是线程越多就越好,要根据服务返回的时间,执行时间,CPU核数计算出最佳线程数,
公式:最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU核数
场景2:我们需要获取同一商品,各个商店的报价,针对商店原始报价,获取人民币实时汇率转人民币后并且进行打折后展示最终价格,
1.获取各个商店的报价,同时通过接口获取人民币汇率
2.转人民币后访问打折服务获取最终价格
显然,对于场景2,并行流已经不适用了,那么我们着重看看CompletableFuture
准备:模拟三个服务
//获取价格 private static String getPrice(String shop) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return shop + "获取价格-"; } //获取汇率 private static String getRate(String shop) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return shop + "获取汇率-"; } //打折服务 private static String getDiscount(String shop ) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return shop + "打折服务-"; }
private static void TestCompletable2(List shops) { Executor executor = Executors.newFixedThreadPool(shops.size()); List> collect = shops.stream() .map(shop -> CompletableFuture.supplyAsync(() -> getPrice(shop), executor) .thenCombine(CompletableFuture.supplyAsync(() -> getRate(shop), executor), (str1, str2) -> str1 + str2)) .map(future -> future.thenCompose(chiStr -> CompletableFuture.supplyAsync(() -> getDiscount(chiStr), executor))).collect(Collectors.toList()); System.out.println(collect.stream().map(CompletableFuture::join).collect(Collectors.toList())); } public static void main(String[] args) throws InterruptedException { List shops=new ArrayList<>(); shops.add("商店1-"); shops.add("商店2-"); shops.add("商店3-"); shops.add("商店4-"); shops.add("商店5-"); shops.add("商店6-"); shops.add("商店7-"); long startTime = System.currentTimeMillis(); TestCompletable2(shops); System.out.println("耗时:"+(System.currentTimeMillis()-startTime)+"ms"); }耗时:6个商店,3049ms,7个商店,3051ms
解释一下:getPrice和getRate是同时开两个线程调用,只要线程数够这两步是同时执行的,所以只花1秒,另外getDiscount服务再花1秒,共2秒。
经过主编测试,商店20个,30个或者40个,都只耗时2秒多,因为线程池是用100的,大家在开发时,要实事求是,选择适合的线程数。
思考:
1.如何等到所有线程都返回结果后,再执行下面逻辑.
2.如何做到只要有一个线程返回结果就不再等待,立刻执行下面逻辑,这种速度快,给客服体验感超棒.
有需要的可以联系小编一起探讨,因能力有限,请多多指教!