java 异步编程_Java 异步编程 Future

近几天需要做一个报表,报表中的各组数据都是从 ELK 中获取,由于每次查询 ELK 都要花费一段时间,顺序处理任务叠加花费大量时间。接口做完后访问速度特别慢,没有很好的优化思路,直到老大给我代码重构之后接口的访问速度快了5倍左右,被老大吐槽代码写的烂。这谁受的了啊,得好好改好好学,咱今天就先学一下 Future 异步,以后这种优化不能麻烦老大。

Future 异步编程简介

Future 表示异步计算的结果。Future 提供了一些方法来检查计算是否完成、等待计算完成以及检索计算结果。可以使用 get 方法检索结果,它会在计算完成后获取计算结果。它的 cancel 方法可以执行取消操作。它的作用就是:你先干点别的,等我做完了就把工作交接给你进行后续处理。

一个虚构的 future 的用法例子:

interface ArchiveSearcher { String search(String target); }

class App {

ExecutorService executor = ...

ArchiveSearcher searcher = ...

void showSearch(String target) throws InterruptedException {

Callable task = () -> searcher.search(target);

Future future = executor.submit(task);

displayOtherThings(); // do other things while searching

try {

displayText(future.get()); // use future

} catch (ExecutionException ex) { cleanup(); return; }

}

}

displayText(future.get())  这段代码中所需要的的 future.get() 值是通过 get() 从 future 中获取的,它会阻塞程序继续执行直到这个值准备好。

另一个例子

public class SquareCalculator {

private ExecutorService executor

= Executors.newSingleThreadExecutor();

public Future calculate(Integer input) {

return executor.submit(() -> {

Thread.sleep(1000);

return input * input;

});

}

}

Callable 是一个接口,表示一个返回结果的任务,本例中使用 lambda 表达式 () -> {

Thread.sleep(1000);return input * input;} 创建了它的实例。Callable 实例需要传递给一个执行器 Executor ,该执行器将负责在新线程中启动该任务,并返回 Future 结果对象。

有多种方法可以获取 ExecutorService 实例,其中一种是由 Executors 类静态工厂方法提供。在本例中,我们使用了基本的 newSingleThreadExecutor(),它创建一个执行器 ExecutorService,该执行器使用单线程操作一个队列。

一旦我们有了 ExecutorService对象,我们只需要调用 submit(),将可调用对象作为参数传递。submit() 将负责启动任务并返回一个 FutureTask 对象。

FutureTask 类是实现了 Runnable 的 Future 的一个实现,因此可以由一个执行器执行。

Future 的常用方法

使用 isDone() 或 get() 来获取结果

现在我们需要调用 calculate() 方法,并从返回的 Future 对象中获取 Integer 类型的计算结果。Future API 中的有两个方法来帮助我们做这个工作。

Future.isDone()  来告诉我们执行器是否已经完成任务处理。如果任务已经完成会返回 true,否则返回 false。

Future.get() 用来获取实际的计算结果。注意执行该方法后程序会阻塞进程直到任务计算完成并返回结果。我们在编程时最好先使用 isDone() 方法来判断是否计算完成,防止程序阻断其他任务。

Future future = new SquareCalculator().calculate(10);

while(!future.isDone()) {

System.out.println("Calculating...");

Thread.sleep(300);

}

Integer result = future.get();

get() 方法还有一个重载方法。

Integer result = future.get(500, TimeUnit.MILLISECONDS);

get(long, TimeUnit) 和 get() 的区别在于,如果任务没有在指定的超时时间之前返回,前者将抛出一个TimeoutException。

使用 cancel() 取消任务

假设我们已经触发了一个任务,但是由于某些原因,我们不再关心结果。我们可以使用Future.cancel(boolean) 来告诉执行程序停止操作并中断其底层线程

Future future = new SquareCalculator().calculate(4);

boolean canceled = future.cancel(true);

如果我们尝试在调用 cancel() 之后调用 get(),会出现 CancellationException。我们可以使用 Future. iscancelled() 查看一个 Future 是否已经被取消。

调用 cancel()可能会失败,这时会返回 false。cancel() 接受一个布尔值作为参数,它来控制执行该任务的线程是否应该被中断。

实现多线程

我们当前的ExecutorService是单线程的,因为它是通过Executors.newSingleThreadExecutor 获得的。为了明确是否是单一线程,让我们同时触发两个任务计算。

SquareCalculator squareCalculator = new SquareCalculator();

Future future1 = squareCalculator.calculate(10);

Future future2 = squareCalculator.calculate(100);

while (!(future1.isDone() && future2.isDone())) {

System.out.println(

String.format(

"future1 is %s and future2 is %s",

future1.isDone() ? "done" : "not done",

future2.isDone() ? "done" : "not done"

)

);

Thread.sleep(300);

}

Integer result1 = future1.get();

Integer result2 = future2.get();

System.out.println(result1 + " and " + result2);

squareCalculator.shutdown();

分析一下返回结果

calculating square for: 10

future1 is not done and future2 is not done

future1 is not done and future2 is not done

future1 is not done and future2 is not done

future1 is not done and future2 is not done

calculating square for: 100

future1 is done and future2 is not done

future1 is done and future2 is not done

future1 is done and future2 is not done

100 and 10000

我们可以发现这两个任务不是并行的,第二个任务会在第二个任务执行完成之后开始,整个过程大约需要 2 秒。

要让我们的程序实现多线程并行运行,我们需要使用支持多线程的 ExecutorService,我们可以使用 Executors.newFixedThreadPool() 来实现多线程,该方法可以传递开启线程的个数。

public class SquareCalculator {

private ExecutorService executor = Executors.newFixedThreadPool(2);

//...

}

现在我们有了一个能够同时使用两个线程的执行器。如果我们再次运行完全相同的客户机代码,我们将得到以下输出

calculating square for: 10

calculating square for: 100

future1 is not done and future2 is not done

future1 is not done and future2 is not done

future1 is not done and future2 is not done

future1 is not done and future2 is not done

100 and 10000

现在看起来这两个任务是同时启动和完成运行的,整个过程大约需要1秒才能完成。

还有其他工厂方法可用于创建线程池,如 Executors.newCachedThreadPool(),它在以前使用的线程可用时重用它们,以及 Executors.newScheduledThreadPool(),它调度命令在给定延迟后运行。

Future 的有趣用法

另一个使用 Future 计算阶乘的例子 FactorialCalculator.java

import java.math.BigInteger;

import java.util.concurrent.Callable;

import java.util.concurrent.TimeUnit;

public class FactorialCalculator implements Callable {

private int value;

public FactorialCalculator(int value) {

this.value = value;

}

@Override

public BigInteger call() throws Exception {

var result = BigInteger.valueOf(1);

if (value == 0 || value == 1) {

result = BigInteger.valueOf(1);

} else {

for (int i = 2; i <= value; i++) {

result = result.multiply(BigInteger.valueOf(i));

}

}

TimeUnit.MILLISECONDS.sleep(500);

//降低程序计算速度

return result;

}

}

FactorialCalculator 使用 BigInteger 计算阶乘。

public class FactorialCalculator implements Callable {

FactorialCalculator 实现了一个 Callable 对象。 Callable 对象表示一个返回结果的异步任务。在我们的例子中,结果是计算的阶乘。

使用 Future 对象 JavaFutureEx.java

import java.math.BigInteger;

import java.util.ArrayList;

import java.util.HashMap;

import java.util.List;

import java.util.Map;

import java.util.Random;

import java.util.concurrent.ExecutionException;

import java.util.concurrent.Executors;

import java.util.concurrent.Future;

import java.util.concurrent.ThreadPoolExecutor;

public class JavaFutureEx {

public static void main(String[] args) throws ExecutionException, InterruptedException {

var executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);

var random = new Random();

for (int i = 0; i < 6; i++) {

int number = random.nextInt(100) + 10;

var factorialCalculator = new FactorialCalculator(number);

Map> result = new HashMap<>();

result.put(number, executor.submit(factorialCalculator));

resultList.add(result);

}

for (Map> pair : resultList) {

var optional = pair.keySet().stream().findFirst();

if (!optional.isPresent()) {

return;

}

var key = optional.get();

System.out.printf("Value is: %d%n", key);

var future = pair.get(key);

var result = future.get();

var isDone = future.isDone();

System.out.printf("Result is %d%n", result);

System.out.printf("Task done: %b%n", isDone);

System.out.println("--------------------");

}

executor.shutdown();

}

}

代码解析

var executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);

var 是 java 10 出现的局部变量定义方法。executor 服务处理异步任务的生命周期。它的submit() 既可以接受可调用对象,也可以接受可运行对象。

var factorialCalculator = new FactorialCalculator(number);

创建了一个 FactorialCalculator 任务。它将异步运行。

Map> result = new HashMap<>();

result.put(number, executor.submit(factorialCalculator));

resultList.add(result);

我们将任务提交给 executor ,将整数值和 future 放在一个 Map 中,这样我们就有了数值和计算的阶乘的 Map。我们可以看一下结果列表,注意 future 的值还没有被计算完就已经返回了。

var optional = pair.keySet().stream().findFirst();

if (!optional.isPresent()) {

return;

}

var key = optional.get();

我们可以使用 get() 方法获取计算完成的结果值,当我们调用get()时,处理将被阻塞,直到获取值为止。

var future = pair.get(key);

var result = future.get();

如上的例子返回结果如下

Value is: 39

Result is 20397882081197443358640281739902897356800000000

Task done: true

--------------------

Value is: 99

Result is 933262154439441526816992388562667004907159682643816214685929638952175999932299156089414639761565182862536979208272237582511852109168640000000000000000000000

Task done: true

--------------------

Value is: 39

Result is 20397882081197443358640281739902897356800000000

Task done: true

--------------------

Value is: 102

Result is 961446671503512660926865558697259548455355905059659464369444714048531715130254590603314961882364451384985595980362059157503710042865532928000000000000000000000000

Task done: true

--------------------

Value is: 12

Result is 479001600

Task done: true

--------------------

Value is: 49

Result is 608281864034267560872252163321295376887552831379210240000000000

Task done: true

--------------------

总结

通过如上的两个例子我们应该可以掌握 Future 异步编程的常见用法,将其用到日常的编程处理中完全够用,如果需要详细了解 Future 的实现可以学习 Fork/Join Framework in Java ,另外 Future 还有一个 CompletableFuture 的实现,是 Java 8 中加入的,有更多的 API 方法实现复杂操作,详情可以查看 Guide To CompletableFuture。

参考资料

Java Futrure Tutorial

Java 11 官方文档

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值