Java 8系列:
Java 8系列之Lambda表达示
Java 8系列之StreamApi
Java 8系列之Collector
Java 8系列之Optional
Java 8系列之Future
一.基本概念
1.并发与并行
2.同步API与异步API
同步API:你调用了某个方法,调用方在被调用方运行的过程中会等待,被调用方运行结束返回,调用方取得被调用方的返回值并继续运行。即使调用方和被调用方在不同的线程中运行,调用方还是需要等待被调用方结束运行,这就是阻塞式调用。
异步API:你调用了某个方法,被调用方直接返回,或者至少在被调用方计算完成之前,将它剩余的计算任务交给另一个线程去做,该线程和调用方是异步的,这就是非阻塞式调用。
二.异步调用
1.java8之前实现异步调用的方式
首先写一个简单的例子来了解异步调用:使用Future以异步的方式执行一个耗时的操作,这种编程方式让我们的线程可以在ExecutorService以并发方式调用另一个线程执行耗时操作的同时,去执行一些其他的任务。
//创建Executor-Service ,通过它你可以向线程池提交任务
ExecutorService executor = Executors.newCachedThreadPool();
@Test
public void test1() {
long start = System.nanoTime();
//向 Executor-Service 提交一个Callable 对象
Future<Double> future = executor.submit(new Callable<Double>() {
public Double call() {
//以异步方式在新的线程中执行耗时的操作
return doSomeLongComputation(start);
}
});
//异步操作进行的同时,你可以做其他的事情
doSomethingElse(start);
try {
//获取异步操作的结果,如果最终被阻塞,无法得到结果,那么在最多等待1秒钟之后退出
Double result = future.get(1, TimeUnit.SECONDS);
System.out.println("全部计算完成,耗时:"+ (System.nanoTime() - start) / 1_000_000 + " msecs");
} catch (ExecutionException ee) {
// 计算抛出一个异常
} catch (InterruptedException ie) {
// 当前线程在等待过程中被中断
} catch (TimeoutException te) {
// 在Future对象完成之前超过已过期
}
}
public double doSomeLongComputation(Long start) {
delay();
System.out.println("异步执行一个长的计算,耗时:" + (System.nanoTime() - start) / 1_000_000 + " msecs");
return 0.00;
}
public void doSomethingElse(Long start) {
delay();
System.out.println("当前线程做别的计算,耗时:"+ (System.nanoTime() - start) / 1_000_000 + " msecs");
}
public static void delay() {
try {
Thread.sleep( (long) (Math.random() * 1000));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
结果:
2.使用java8的CompletableFuture来实现异步调用
private double calculatePrice(String product) {
delay();
return new Random().nextDouble() * product.charAt(0) + product.charAt(1);
}
public Future<Double> getPriceAsync(String product) {
//创建 CompletableFuture对象,它会包含计算的结果
CompletableFuture<Double> futurePrice = new CompletableFuture<>();
//在另一个线程中以异步方式执行计算
new Thread( () -> {
System.out.println("异步做别的计算");
try {
//如果价格计算正常结束,完成 Future 操作并设置商品价格
double price = calculatePrice(product);
//需长时间计算的任务结束并得出结果时,设置Future 的返回值
futurePrice.complete(price);
} catch (Exception ex) {
//否则就抛出导致失败的异常,完成这次 Future 操作
futurePrice.completeExceptionally(ex);
}
}).start();
//无需等待还没结束的计算,直接返回 Future 对象
return futurePrice;
}
public double getPriceDirect(Long start,String product) {
double price = calculatePrice(product);
System.out.println("当前线程去查询羽毛球拍的价格,耗时:"+ (System.nanoTime() - start) / 1_000_000 + " msecs");
return price;
}
@Test
public void test2() {
long start = System.nanoTime();
Future<Double> futurePrice = getPriceAsync("羽毛球");
long invocationTime = ((System.nanoTime() - start) / 1_000_000);
System.out.println("异步去查询羽毛球的价格,耗时: " + invocationTime + " msecs");
// 执行更多任务,比如查询其他商店
double priceDirect = getPriceDirect(start, "羽毛球拍");
// 在计算商品价格的同时
try {
double priceAsync = futurePrice.get();
System.out.printf("羽毛球跟羽毛球拍的总价格是: %.2f%n", priceAsync+priceDirect);
} catch (Exception e) {
throw new RuntimeException(e);
}
long retrievalTime = ((System.nanoTime() - start) / 1_000_000);
System.out.println("Price returned after " + retrievalTime + " msecs");
}
结果:
其中CompletableFuture类自身提供了大量精巧的工厂方法,使用这些方法能更容易地完成整个流程,还不用担心实现的细节。
比如,采用supplyAsync方法后,可以用一行语句重写代码清单11中的getPriceAsync方法,如下所示。
//使用工厂方法 supplyAsync 创建 CompletableFuture 对象
public Future<Double> getPriceAsync(String product) {
return CompletableFuture.supplyAsync(() -> calculatePrice(product));
}
supplyAsync方法接受一个生产者(Supplier)作为参数,返回一个CompletableFuture对象,该对象完成异步执行后会读取调用生产者方法的返回值。生产者方法会交由ForkJoinPool池中的某个执行线程(Executor)运行,但是也可以使用supplyAsync方法的重载版本,传递第二个参数指定不同的执行线程执行生产者方法。一般而言,向CompletableFuture的工厂方法传递可选参数,指定生产者方法的执行线程是可行的。
三.比较顺序执行,并行,并发–异步执行,并发–自定义异步执行的速度
public double getPrice(String product) {
return calculatePrice(product);
}
List<String> shopNames = Arrays.asList("北京华联",
"华润",
"沃尔玛",
"大润发",
"万果园",
"一峰");
/**
* 使用流顺序计算
* @param product
* @return
*/
public List<String> findPrices(String product) {
return shopNames.stream()
.map(shopName -> String.format("%s 价格: %.2f",
shopName, getPrice(product)))
.collect(toList());
}
/**
* 使用流并行计算
* @param product
* @return
*/
public List<String> findPricesParallel(String product) {
return shopNames.parallelStream()
.map(shopName -> String.format("%s 价格: %.2f",
shopName, getPrice(product)))
.collect(toList());
}
/**
* 异步运算
* @param product
* @return
*/
public List<String> findPricesFuture(String product) {
List<CompletableFuture<String>> priceFutures = shopNames.stream()
.map(shopName -> CompletableFuture.supplyAsync(
() -> String.format("%s 价格: %.2f", shopName, getPrice(product))))
.collect(toList());
//CompletableFuture 类中的 join 方法和 Future 接口中的 get 有相同的含义
return priceFutures.stream()
.map(CompletableFuture::join)
.collect(toList());
}
private final Executor executor1 =
//创建一个线程池,线程池中线程的数目为100和商店数目二者中较小的一个值
Executors.newFixedThreadPool(Math.min(shopNames.size(), 100),
new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
//使用守护线程——这种方式不会阻止程序的关停
t.setDaemon(true);
return t;
}
});
/**
* 异步运算:使用定制的执行器:调整线程池的大小
* @param product
* @return
*/
public List<String> findPricesFuture1(String product) {
List<CompletableFuture<String>> priceFutures = shopNames.stream()
.map(shopName -> CompletableFuture.supplyAsync(() -> String.format("%s 价格: %.2f", shopName, getPrice(product)), executor1))
.collect(toList());
return priceFutures.stream()
.map(CompletableFuture::join)
.collect(toList());
}
@Test
public void test3() {
long start = System.nanoTime();
System.out.println(findPrices("羽毛球"));
System.out.println("Done in " + (System.nanoTime() - start) / 1_000_000 + " msecs");
start = System.nanoTime();
System.out.println(findPricesParallel("羽毛球"));
System.out.println("Done in 并行:" + (System.nanoTime() - start) / 1_000_000 + " msecs");
start = System.nanoTime();
System.out.println(findPricesFuture("羽毛球"));
System.out.println("Done in 并发:" + (System.nanoTime() - start) / 1_000_000 + " msecs");
//并行和并发不相伯仲,究其原因都一样:它们内部采用的是同样的通用线程池,默认都使用固定数目的线程,具体线程数取决于
// Runtime.getRuntime().availableProcessors() 的返回值。
// 然而, CompletableFuture 具有一定的优势,因为它允许你对执行器( Executor )进行配置,尤其是线程池的大小
start = System.nanoTime();
System.out.println(findPricesFuture1("羽毛球"));
System.out.println("Done in 并发(定制的执行器:调整线程池的大小):" + (System.nanoTime() - start) / 1_000_000 + " msecs");
}
结果: