使用CompletableFuture构建异步应用 (2)

(接上文)

现在,我们假设每张饼都需要烙正、反两面,每一面烙熟需要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/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值