我正在玩Java8的流和CompletableFuture
。我之前存在的代码有一个类,它接受一个URL并下载它:
public class FileDownloader implements Runnable {
private URL target;
public FileDownloader(String target) {
this.target = new URL(target);
}
public void run() { /* do it */ }
}
现在,这个类从另一个发出List<String>
的部分(单个主机上的多个目标)获取它的信息。
我已将周围的代码切换为CompletableFuture
:
public class Downloader {
public static void main(String[] args) {
List<String> hosts = fetchTargetHosts();
for (String host : hosts) {
HostDownloader worker = new HostDownloader(host);
CompletableFuture<List<String>> future =
CompletableFuture.supplyAsync(worker);
future.thenAcceptAsync((files) -> {
for (String target : files) {
new FileDownloader(target).run();
}
});
}
}
public static class HostDownloader implements Supplier<List<String>> {
/* not shown */
}
/* My implementation should either be Runnable or Consumer.
Please suggest based on a idiomatic approach to the main loop.
*/
public static class FileDownloader implements Runnable, Consumer<String> {
private String target;
public FileDownloader(String target) {
this.target = target;
}
@Override
public void run() { accept(this.target); }
@Override
public void accept(String target) {
try (Writer output = new FileWriter("/tmp/blubb")) {
output.write(new URL(target).getContent().toString());
} catch (IOException e) { /* just for demo */ }
}
}
}
现在,这并不自然。我正在制作String
s流,而我的FileDownloader
一次消耗其中一个。是否有现成品可以让我的单一值Consumer
与List
一起使用,或者我在这里停留for
循环?
我知道将循环移到accept
而只是制作Consumer<List<String>>
是微不足道的,这不是重点。
答案 0 :(得分:3)
将两个直接相关的步骤分解为两个异步步骤是没有意义的。他们仍然依赖,如果分离有任何影响,那就不会是积极的。
您只需使用
即可List<String> hosts = fetchTargetHosts();
FileDownloader fileDownloader = new FileDownloader();
for(String host: hosts)
CompletableFuture.runAsync(()->
new HostDownloader(host).get().forEach(fileDownloader));
或者,假设FileDownloader
没有关于下载的可变状态:
for(String host: hosts)
CompletableFuture.runAsync(()->
new HostDownloader(host).get().parallelStream().forEach(fileDownloader));
这仍然具有与使用supplyAsync
加thenAcceptAsync
的原始方法相同的并发级别,因为这两个相关步骤无论如何都无法同时运行,因此简单的解决方案是将两个步骤放在一起进入一个将以异步方式执行的简洁操作。
但是,此时值得注意的是,不建议在此操作中使用CompletableFuture
。正如it’s documentation所述:
- 使用
执行没有显式Executor参数的所有 async 方法ForkJoinPool.commonPool()
公共池的问题在于,其预配置的并发级别取决于CPU核心数,如果在I / O操作期间阻塞线程,则不会调整它。换句话说,它不适合I / O操作。
与Stream
不同,CompletableFuture
允许您为异步操作指定Executor
,因此您可以将自己的Executor
配置为另一方面,当你处理Executor
时,适用于I / O操作,根本不需要CompletableFuture
,至少不是这么简单的任务:
List<String> hosts = fetchTargetHosts();
int concurrentHosts = 10;
int concurrentConnections = 100;
ExecutorService hostEs=Executors.newWorkStealingPool(concurrentHosts);
ExecutorService connEs=Executors.newWorkStealingPool(concurrentConnections);
FileDownloader fileDownloader = new FileDownloader();
for(String host: hosts) hostEs.execute(()-> {
for(String target: new HostDownloader(host).get())
connEs.execute(()->fileDownloader.accept(target));
});
在这个地方,您可以考虑将FileDownloader.accept
的代码内联到lambda表达式中或将其还原为Runnable
,以便您可以将内部循环的语句更改为{{1 }}
答案 1 :(得分:1)
我认为你需要像这样做:
for (String host : hosts) {
HostDownloader worker = new HostDownloader(host);
CompletableFuture<List<String>> future =
CompletableFuture.supplyAsync(worker);
future.thenAcceptAsync(files ->
files.stream()
.forEach(target -> new FileDownloader(target).run())
);
}
顺便说一句,你可以对主循环做同样的事情......
编辑: 由于OP编辑了原帖,添加了FileDownloader的实现细节,我正在编辑我的答案。 Java 8功能接口旨在允许使用lambda expr代替具体的Class。它并不意味着像常规界面9那样实现,尽管它可以是,因此,&#34;利用&#34; Java 8使用者意味着将FileDownloader替换为接受代码,如下所示:
for (String host : hosts) {
HostDownloader worker = new HostDownloader(host);
CompletableFuture<List<String>> future = CompletableFuture.supplyAsync(worker);
future.thenAcceptAsync(files ->
files.forEach(target -> {
try (Writer output = new FileWriter("/tmp/blubb")) {
output.write(new URL(target).getContent().toString());
} catch (IOException e) { /* just for demo */ }
})
);
}
答案 2 :(得分:1)
另一种选择是:
CompletableFuture.supplyAsync(worker)
.thenApply(list -> list.stream().map(FileDownloader::new))
.thenAccept(s -> s.forEach(FileDownloader::run));