(接上文)
现在,我们假设每张饼都需要烙正、反两面,每一面烙熟需要1秒钟:
package package0614;
import java.util.Random;
public class Dabing2 {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Dabing2(String name) {
this.name = name;
}
public double laoZheng() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return this;
}
public double laoFan() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return new Random().nextDouble() * 100;
}
}
第1次尝试,使用stream烙5张饼:
public static void main(String args[]) {
long starttime = System.nanoTime();
List<Dabing2> list = Arrays.asList(new Dabing2("A"),
new Dabing2("B"),
new Dabing2("C"),
new Dabing2("D"),
new Dabing2("E"));
List<String> results = list.stream().map(e -> e.laoZheng()).map(e -> e.getName() + ": " + e.laoFan())
.collect(Collectors.toList());
System.out.println(results);
long endtime = System.nanoTime();
System.out.println("Total time: " + (endtime - starttime) / 1000000 + "ms");
}
运行结果如下:
[A: 55.4523443439747, B: 49.47515167835467, C: 63.02314261631643, D: 3.789149628644528, E: 73.51160030525537]
Total time: 10086ms
不出所料,需要10秒钟时间。
第2次尝试,使用parallelStream烙5张饼:
public static void main(String args[]) {
long starttime = System.nanoTime();
List<Dabing2> list = Arrays.asList(new Dabing2("A"),
new Dabing2("B"),
new Dabing2("C"),
new Dabing2("D"),
new Dabing2("E"));
List<String> results = list.parallelStream().map(e -> e.laoZheng()).map(e -> e.getName() + ": " + e.laoFan())
.collect(Collectors.toList());
System.out.println(results);
long endtime = System.nanoTime();
System.out.println("Total time: " + (endtime - starttime) / 1000000 + "ms");
}
运行结果如下:
[A: 37.39151716337421, B: 15.775742198107622, C: 31.667344024582167, D: 14.386117456032943, E: 5.913311718908054]
Total time: 2060ms
不出所料,需要2秒钟时间。
第3次尝试:使用CompletableFuture:
public static void main(String args[]) {
long starttime = System.nanoTime();
List<Dabing2> list = Arrays.asList(new Dabing2("A"),
new Dabing2("B"),
new Dabing2("C"),
new Dabing2("D"),
new Dabing2("E"));
List<CompletableFuture<String>> listFuture = new ArrayList<>();
for (Dabing2 dabing2 : list) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> dabing2.laoZheng())
.thenApply(e -> e.getName() + ": " + e.laoFan());
listFuture.add(future);
}
List<String> results = listFuture.stream().map(CompletableFuture::join).collect(Collectors.toList());
System.out.println(results);
long endtime = System.nanoTime();
System.out.println("Total time: " + (endtime - starttime) / 1000000 + "ms");
}
运行结果如下:
[A: 91.15306335229461, B: 24.368638002069932, C: 61.34350781335267, D: 36.89446490304583, E: 51.432231549457605]
Total time: 2054ms
不出所料,需要2秒钟时间。
第4次尝试:如果换成10张饼呢?代码略,运行结果如下:
[A: 96.13014798074259, B: 69.23653704155743, C: 62.89131092514255, D: 57.929321535499376, E: 72.87524123696774, F: 71.44149392741076, G: 57.32601348732903, H: 83.38410150602131, I: 94.85882057820052, J: 50.351187872048044]
Total time: 4066ms
为什么需要4秒钟,而不是3秒钟呢?原因在于,thenApply()方法的操作,是和之前的Future在同一个线程里面串行工作的。这也就是文章最开头提到的第1种方法。
我们可以把CPU想象成坑,线程就是小碗,饼是放在小碗里,然后放在锅上烙。烙完A的正面,接着烙A的反面。烙完B的正面,接着烙B的反面。导致C一直在等待。而等到C开始工作,已经有小碗和坑空闲了,但是C的正反面只能串行烙,所以一共需要4秒钟。
有的同学说了,这是因为没有配置Executor的并发线程数,默认值是8个线程,所以花了4秒钟,如果配置100个线程,那么10个任务线程就可以并发运行了。这种说法也是正确的。请思考一下一共需要花多长时间?代码如下:
第5次尝试:使用100个并发线程烙10张饼:
private static final Executor executor = Executors.newFixedThreadPool(100, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}
});
public static void main(String args[]) {
long starttime = System.nanoTime();
List<Dabing2> list = Arrays.asList(new Dabing2("A"),
new Dabing2("B"),
new Dabing2("C"),
new Dabing2("D"),
new Dabing2("E"),
new Dabing2("F"),
new Dabing2("G"),
new Dabing2("H"),
new Dabing2("I"),
new Dabing2("J")
);
List<CompletableFuture<String>> listFuture = new ArrayList<>();
for (Dabing2 dabing2 : list) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> dabing2.laoZheng(), executor)
.thenApply(e -> e.getName() + ": " + e.laoFan());
listFuture.add(future);
}
List<String> results = listFuture.stream().map(CompletableFuture::join).collect(Collectors.toList());
System.out.println(results);
long endtime = System.nanoTime();
System.out.println("Total time: " + (endtime - starttime) / 1000000 + "ms");
}
运行结果如下:
[A: 67.70133238190905, B: 93.327445420466, C: 80.98853579650296, D: 9.89354886325099, E: 66.33315575268045, F: 24.320805719321825, G: 65.34780373758679, H: 2.3678179401899424, I: 76.08893246211903, J: 89.53849243315845]
Total time: 2057ms
希望你对2秒钟的结果并不感到意外。
回到一开始的问题,现在假如线程不能(或者说不需要)切换CPU(比如说是CPU密集型的任务,没有等待时间,即使切换CPU,也无法提高性能),在这种情况下,烙10张饼需要多长时间?
为了模拟这种情况,我们只允许并发8个任务线程,每个任务线程一个CPU,这样就不会切换CPU了(会和主线程切换CPU,不过主线程忽略不计)。代码如下(只需把100改成8):
第6次尝试:模拟禁止线程切换CPU的效果:
private static final Executor executor = Executors.newFixedThreadPool(8, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}
});
public static void main(String args[]) {
long starttime = System.nanoTime();
List<Dabing2> list = Arrays.asList(new Dabing2("A"),
new Dabing2("B"),
new Dabing2("C"),
new Dabing2("D"),
new Dabing2("E"),
new Dabing2("F"),
new Dabing2("G"),
new Dabing2("H"),
new Dabing2("I"),
new Dabing2("J")
);
List<CompletableFuture<String>> listFuture = new ArrayList<>();
for (Dabing2 dabing2 : list) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> dabing2.laoZheng(), executor)
.thenApply(e -> e.getName() + ": " + e.laoFan());
listFuture.add(future);
}
List<String> results = listFuture.stream().map(CompletableFuture::join).collect(Collectors.toList());
System.out.println(results);
long endtime = System.nanoTime();
System.out.println("Total time: " + (endtime - starttime) / 1000000 + "ms");
}
运行结果如下:
[A: 30.647253989773404, B: 11.389428951618285, C: 46.735632304796084, D: 49.74105190825268, E: 49.666923965637665, F: 80.69750116195178, G: 89.21920560287695, H: 98.87025808386537, I: 62.00460695058156, J: 47.47971523961366]
Total time: 4064ms
果然,又变成4秒钟了。
那么,怎么才能让结果像文章一开头提到的第2种方法那样,只用3秒钟呢?
答案是把thenApply()方法换成thenCompose()方法。
- thenApply(): 和之前的Future在同一个线程里面串行工作;
- thenCompose(): 把2个异步操作给串起来;
第7次尝试:使用thenCompose()方法:
private static final Executor executor = Executors.newFixedThreadPool(8, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}
});
public static void main(String args[]) {
long starttime = System.nanoTime();
List<Dabing2> list = Arrays.asList(new Dabing2("A"),
new Dabing2("B"),
new Dabing2("C"),
new Dabing2("D"),
new Dabing2("E"),
new Dabing2("F"),
new Dabing2("G"),
new Dabing2("H"),
new Dabing2("I"),
new Dabing2("J")
);
List<CompletableFuture<String>> listFuture = new ArrayList<>();
for (Dabing2 dabing2 : list) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> dabing2.laoZheng(), executor)
.thenCompose(e -> CompletableFuture.supplyAsync(() -> e.getName() + ": " + e.laoFan(), executor));
listFuture.add(future);
}
List<String> results = listFuture.stream().map(CompletableFuture::join).collect(Collectors.toList());
System.out.println(results);
long endtime = System.nanoTime();
System.out.println("Total time: " + (endtime - starttime) / 1000000 + "ms");
}
运行结果如下:
[A: 77.32257198460907, B: 94.7966134735508, C: 98.4247304567527, D: 58.687981756690675, E: 10.705884917172337, F: 2.66460909938242, G: 94.47073933921335, H: 97.08196979049613, I: 43.836353064485756, J: 92.17049504889374]
Total time: 3064ms
终于,实现了文章一开头所说的,烙3张饼最少需要3秒钟的情况。
第8次尝试,另一种写法:
public static void main(String args[]) {
long starttime = System.nanoTime();
List<Dabing2> list = Arrays.asList(new Dabing2("A"),
new Dabing2("B"),
new Dabing2("C"),
new Dabing2("D"),
new Dabing2("E"),
new Dabing2("F"),
new Dabing2("G"),
new Dabing2("H"),
new Dabing2("I"),
new Dabing2("J")
);
List<CompletableFuture<String>> listFuture = list.stream().map(e -> CompletableFuture.supplyAsync(() -> e.laoZheng(), executor))
.map(future -> future.thenCompose(e2 -> CompletableFuture.supplyAsync(() -> e2.getName() + ": " + e2.laoFan(), executor))).collect(Collectors.toList());
List<String> results = listFuture.stream().map(CompletableFuture::join).collect(Collectors.toList());
System.out.println(results);
long endtime = System.nanoTime();
System.out.println("Total time: " + (endtime - starttime) / 1000000 + "ms");
}
结果也是一样的。
具体说来,烙完A的正面之后,并不会在同一个线程(碗)里烙A的反面,而是线程结束(碗清空)了,同时新启动了一个任务,去烙A的反面,也就是说这个任务是和其它任务来竞争线程(碗)的。
其实,在这种情况下,结果还是有一定的概率会出现4秒钟。因为大家都是在竞争,保不齐会出现A和B的正反面都烙好了,而C连一面还没有烙的情况。
另外再提一下另外一个方法:thenCombine(),它跟thenCompose()方法相似,也是把2个CompletableFuture对象串起来,但是后者要求二者有依赖关系,而前者则要求二者之间没有依赖关系。
最后提一下响应CompletableFuture的completion事件。在前面的代码中,都是等到所有的饼都烙好之后,才会打印出结果。如果希望在第一时间就有响应,也就是在第1张饼烙好之后就立刻告诉用户,该如何实现呢?
答案是使用thenAccept()方法,代码如下:
public static void main(String args[]) {
long starttime = System.nanoTime();
List<Dabing2> list = Arrays.asList(new Dabing2("A"),
new Dabing2("B"),
new Dabing2("C"),
new Dabing2("D"),
new Dabing2("E"),
new Dabing2("F"),
new Dabing2("G"),
new Dabing2("H"),
new Dabing2("I"),
new Dabing2("J")
);
Stream<CompletableFuture<String>> streamFuture =
list.stream().map(e -> CompletableFuture.supplyAsync(() -> e.laoZheng(), executor))
.map(future -> future.thenCompose(e2 -> CompletableFuture.supplyAsync(() -> e2.getName() + ": " + e2.laoFan(), executor)));
CompletableFuture[] futures =
streamFuture.map(f -> f.thenAccept(System.out::println)).toArray(size -> new CompletableFuture[size]);
CompletableFuture.allOf(futures).join();
long endtime = System.nanoTime();
System.out.println("Total time: " + (endtime - starttime) / 1000000 + "ms");
}
运行结果如下:
F: 2.4167631052547156
A: 56.264328725632616
D: 68.54896173377098
E: 0.6471072032815384
B: 97.82969628277193
C: 19.440428834510072
H: 14.132576179967293
G: 96.93087826713057
J: 15.148461531583257
I: 37.7317285366745
Total time: 3065ms
注:把线程数改小,比如1或者2,效果更加明显。
参考
Java 8 in Action: https://livebook.manning.com/book/java-8-in-action/