Tech&Wk_Program_Java_并发

目标:处理并发,目标实在同一个CPU上执行多个松耦合的任务,在等待远程服务的结果或者查询数据库时,尽可能让这些核都忙起来,

从而最大化应用的吞吐量,尽量避免线程阻塞和浪费宝贵的计算资源.

执行比较耗时的操作时,尤其是那些依赖一个或多个远程服务的操作,使用异步任务可以改善程序的性能,加快程序的响应速度。

尽可能为客户提供异步API,使用 CompletableFuture 类提供的特性。能轻松实现这一目标。

Java 提供了两个主要的工具集:

1. Future 接口, 尤其是 CompletableFuture.

2. Flow API 实现了“发布-订阅” 协议,能提供更加细粒度的程序控制.


如果你的程序并未使用多线程,那它同一时刻能有小使用的只有处理器众多核中的一个.

对并行流的迭代是比显式使用线程更高级的概念。使用Stream是对一种线程使用模式的抽象。

Java.util.concurrent.RecursiveTask 对线程的 fork/join进行了抽象,可以并发地执行分而治之的算法。用一种

更高级的方式在多核机器上高效地执行数组求和计算。


线程池:ExecutorService 用户可以提交任务并获取它们的执行结果,期望的实现是使用newFixedThreadPool 这样的工厂创建一个线程池:

ExecutorSerivce newFixedThreadPool(int nThread)

创建一个包含n工作线程的ExecutorSerivce,新创建的线程会被放入一个线程池,每次有新任务请求时,以先来先到的策略从池中选取未被

使用的线程执行提交任务的请求。任务执行完毕之后,这些线程又会被归还给线程池。

最大优势在于能以很低的成本向线程池提交上千个任务,同时保证硬件匹配的任务执行。

可设置队列长度,拒绝策略以及不同任务的优先级。

不足

1. 使用k个线程的线程池只能并发地执行k个任务。提交的任务如果超过了这个限制,线程池不会创建新线程取执行该任务,这些超限的

任务会被加入等待队列,直到现有任务执行完毕才会重新调度空闲线程去执行新任务。

需要特别留意任务是否存在会进入睡眠,等待I/O结束或者等待网络连接的情况。

如果早期提交的任务或者正在执行的任务需要等待后续任务,正是Future典型使用的模式,那么可能会导致线程池死锁。

 

操作时的一个好习惯是在推出程序执行之前,确保关闭每一个线程池,线程池中的工作线程在创建完成后会由于等待另一个任务执行完毕

而无法正常终止。

常使用一个长时间运行的 ExectorService管理需要持续运行的服务。 Thread.setDaemon 守护进程。


CompletableFuture

在多种并发资源(多个CPU核和其他类似资源)可用的情况下,如何从高层视角让程序充分地利用这些资源,而不是让代码充斥着结构混乱,难于维护的线程操作。

并行流以及fork/join并行机制未表达并行进行的高层抽象,可以在程序中并行地遍历集合,或者并行地执行分而治之计算。

Future接口,对将来某个时刻会发生的结果进行建模。

调用方发起远程服务查询时,是无法立刻得到查询结果的。采用Future接口可以对异步计算进行建模,返回一个指向执行结果的引用,运算结束后,调用方可以通过

该引用访问执行的结果。

在Future中触发那些可能耗时的调用,能够将调用线程解放出来,让它们继续执行其他有价值的工作,不必呆呆等待耗时的操作完成。

比底层的Thread更好用。只需将耗时的操作封装在一个Callable对象中,再将它提交给ExecutorSerivce。

线程可以在 ExecutorSerivce 以并发方式调用另一个线程执行耗时操作的同时,去执行一些其他的任务,非依赖。

已经运行到没有异步操做的结果就无法继续任何有意义的工作时,可以调用它的 get 方法去获取操作的结果。如果

操作已经完成,该方法会立刻返回操作的记过,否则它会阻塞你的线程,直到操作完成,返回相应的结果。

isDone() 检测异步计算是否已经结束。

但Future很难表述结果之间的依赖性.

Lambda 表达式及流水线的思想。


1. 提供异步API

2. 使同步API代码变为非阻塞代码。使用流水线将两个连续的异步操作合并为一个异步计算操作。

3. 以响应式的方式处理异步操作的完成实践。

同步API :传统方法调用的另一种称呼。即使调用方和被调用方在不同的线程中运行,调用方还是需要等待被调用方结束运行,也就是阻塞式调用名字的由来。

异步API : 直接返回,至少在被调用方计算完成之前,将它剩余的计算任务交由另一个线程去做,该线程和调用方是异步的-这就是非 阻塞式调用的由来。

执行剩余计算任务的线程会将它的计算结果返回给调用方。

返回的方式要么是通过回调函数,要么是由调用方再次执行一个“等待,直到计算完成”的方法调用。

 

 

进一步:Future 执行完毕可以发送一个通知,仅在计算结果可用时,执行一个Lambda表达式或者方法引用定义的回调函数。

以避免客户端的阻塞

 

 

 

 


 

 

流和ComputableFuture 内部都采用了同样的通用线程池,默认都使用固定数据的线程,具体线程数取决于Runtime.getRuntime().availableProcessors()返回值。

ComputableFuture 允许对执器Executor进行配置,尤其是线程池大小。

创建一个配有线程池的执行器,线程池中的数据取决于:

 

N(线程数) = N(cpu available) *  U (期望CPU利用率 0-1) * (1 + W/C)     等待时间与计算时间比率

如果 99%时间都在等待商店服务响应,估算出的 W/C 比率为 100. 

意味着如果期望 CPU利用率是 100%, 需要创建一个拥有400个线程的线程池.

实际操作中,如果创建的线程比商店的数目更多,反而是一种浪费,因为线程池中有些线程没有机会使用。

建议将执行器使用的线程数,与需要查询的商店数据设定为同一个值。这样每个商店对应一个服务线程。

为避免由于商店的数目过多导致服务器超负荷崩溃,需要设定一个上限,例如100.

 

 

 

处理需大量使用异步操作的情况时,创建更适合应用特性的执行器,几乎时最有效的策略。

 

对集合进行并行计算的两种方式: Stream  CompletableFuture

1. Stream parallel map 操作

2. 枚举出集合中的每一个元素,创建新的线程,在CompletableFuture内对其进行操作。可调整线程池大小,能确保整体的计算不会因为线程都在等待I/O而发生阻塞。

API 使用建议如下:

1. 计算密集型的计算,且没有I/O,推荐Stream接口,效率可能时最高的。如果所有线程都是计算密集型的,就没有必要创建比处理器核数更多的线程。

2.反之,并行的工作单元还涉及等待I/O,包括网络连接等待,那么使用CompletableFuture灵活性更好。根据 等待/计算,W/C的比率设定

需要使用的线程数。这种情况不适用并行流的另一个原因是:处理流的流水线中如果发生I/O等待,流的延迟特性会让我们很难判断到底什么时候触发了等待。


 

构造同步和异步操作

LIst<CompletableFuture<String>> priceFutures = 

  shops.stream().map(shop -> CompletableFuture.supplyAsync( () -> shop.getPrice(product), executor))

          .map(future -> future.thenApply(Quote::parse))

          .map(future -> future.thenCompose(quote-> completableFuture.supplyAsync( () -> Discount.applyDiscoutn(quote) , executor)))

          .collect(toList());

priceFutures.stream().map(CompletableFuture::join).collect(toList());

1. 获取价格。 将lambda 表达式作为参数传递给 supplyAsync 工厂方法就可以以异步方式对shop查询,第一此转换结果是

Stream<CompletableFuture<String>> 

2. 解析报价, 一般解析不涉及任何远程服务,也不会进行I/O,所以能够采用同步操作,不会带来太多延迟,因此对step1 生成的

CompletableFuture 对象调用它的 thenApply.

3.计算折扣。 涉及到远程的 discount 服务,为得到的价格申请折扣,需要远程执行。因此希望异步执行。因此以lambda表达式的方式传递给

supplyAsunc工厂方法,最终返回一个CompletableFuture对象。

thenCompose方法允许对两个异步操作进行流水线。第一个操作完成时,将其结果作为参数传给第二个操作。主线程不阻塞。

Join取得返回值。


 

如果将两个不相干的 CompletableFuture对象结果整合。不希望第一个任务完全结束才开始第二个任务。thenCombine

Future<Double> futurePriceInUSD = 

  CompletableFuture.supplyAsync(() -> shop.getPrice(product))

          .thenCombind( CompletableFuture.supplyAsync( ()-> exchangeSerivice.getRate(Money.EUR,Money.USD)),

                (price,rate) -> price * rate);

BiFunction


 

Future<Double> futurePricesUSD = ...).orTimeout(3, TimeUnit.SECONDS) 

onTimeout 指定超时是,会通过ScheduledThreadExecutor线程结束该 Completable对象,并抛出一个TimeoutException异常,它的

返回值是一个新的CompletableFuture对象. 可以反馈一个友好的消息给用户。

 

Future<Double> futurePriceInUSD = 

  CompletableFuture.supplyAsync(() -> shop.getPrice(product))

          .thenCombind( CompletableFuture.supplyAsync( ()-> exchangeSerivice.getRate(Money.EUR,Money.USD)),

                (price,rate) -> price * rate))

                .orTimeout(3, TimeUnit.SECONDS) ;

 


在发生异常时,保留已有的成果, completeOnTimeout 方法,

Future<Double> futurePriceInUSD = 

  CompletableFuture.supplyAsync(() -> shop.getPrice(product))

          .thenCombind( CompletableFuture.supplyAsync( ()-> exchangeSerivice.getRate(Money.EUR,Money.USD))

          .completeOnTimeOut(DEFAULT_RATE, 1, TimeUnit.SECONDS),

                (price,rate) -> price * rate);


 

CompletableFuture的 completion 事件达到时展示数据.  get / join 会被阻塞。

 Stream<CompletableFuture<String>> findPricesStream(String product);

findPriceStream("arg").map( f -> f.thenAccept(System.out::pringln);

一旦CompletableFuture 计算得到结果,返回一个CompletableFuture<Void>, map 操作返回的是一个

Stream<CompletableFuture<Void>>

CompletableFuture[]  futures = findPriceStream("args")

              .map(f -> f.thenAccept(System.out::pringln))   // 执行业务操作

              .toArray(size -> new CompletableFuture(size));

CompletableFuture.allOf(futures).join();

返回一个 CompletableFuture<Void> 对象。所有执行完毕 , "all tasks returned result or timed out";

anlyOf 返回第一个执行完毕的 CompletableFuture 对象的返回值构成的 CompletableFuture<Object>


 

posted @ 2020-12-05 17:41  君子之行  阅读(30)  评论(0)    收藏  举报