并发编程之 CompletableFuture

在 Java 中,如果需要异步执行任务,可以使用线程来实现,但是我们希望线程执行完之后可以获得执行结果,怎么实现呢?

JDK 1.5 中,引入了 Future 的概念,它可以结合 Callable 接口来获得线程异步执行完成之后的返回值,但它在使用上存在一定的局限性。所以在 JDK1.8 中引入了 CompletableFuture 组件,在 Future 的基础上提供了更加丰富和完善的功能。

1. Future接口

Java5新加的一个接口,它提供了一种异步并行计算的功能,实现类。如果主线程需要执行一个很耗时的计算任务,我们就可以通过 Future把这个任务放到异步线程中执行,主线程继续处理其他任务或者先行结束,再通过 Future获取计算结果。看一下类关系图:

并发编程之 CompletableFuture_第1张图片

1.1 使用

下面,通过代码来演示一下,如何使用:

class MyThread implements Callable {
	@Override
	public String call() throws Exception {
		return "hello!";
	}
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
	FutureTask ft = new FutureTask<>(new MyThread());
	Thread t1 = new Thread(ft);
	t1.start();
	String s = ft.get(); // 获取异步线程的返回结果
	System.out.println(s);
}

运行结果:

并发编程之 CompletableFuture_第2张图片

获取到了异步线程的返回结果“hello”。

1.2 FutureTask 的优缺点

  • 优点

        结合线程池,可以大大提高程序的运行效率;

  • 缺点

① get() 方法阻塞线程,一旦调用不见不散,需要等待异步线程执行完毕,返回处理结果,违背了异步线程提高效率的初衷;

② 如果异步线程耗时较久,主线程又不想等待太长时间,可以通过设置等待时间【get("x",TimeUnit.xx)】进行处理,过时不候,即当达到了设置的等待时间,异步线程仍没有返回结果,直接抛出异常;

③ 等待超时抛出异常的方式,不太优雅,会报出太多的异常日志,可以使用 isDone()方式来告知异步线程的处理状态,当处理完成后,告知主线程处理完成,但是主线程一直轮询等待会耗费无谓的 CPU 资源;

 代码演示:

get()阻塞

class MyThread implements Callable {

	@Override
	public String call() throws Exception {
		System.out.println("异步线程开始执行");
		TimeUnit.SECONDS.sleep(5);
		return "hello callable!!";
	}
}



public static void main(String[] args) throws Exception {
	long start = System.currentTimeMillis();
	FutureTask futureTask = new FutureTask<>(new MyThread());
	Thread thread = new Thread(futureTask);
	thread.start();
	String s = futureTask.get();
	System.out.println("异步线程: " + thread.getName() + "返回的结果:" + s);
	System.out.println("====== 主线程执行结束 ========");
	long end = System.currentTimeMillis();
	System.out.println("========  共耗时 ============ " + (end-start) + "ms");
}

并发编程之 CompletableFuture_第3张图片

可以看到,当调用了异步线程获取返回结果的时候,影响了主线程的运行效率。

get(xx,xx) 等待超时,抛出异常

public static void main(String[] args) throws Exception {
	long start = System.currentTimeMillis();
	FutureTask futureTask = new FutureTask<>(new MyThread());
	Thread thread = new Thread(futureTask);
	thread.start();
	// 主线程,只等待 3s,如果 3s没有响应,则抛出异常
	String s = futureTask.get(3,TimeUnit.SECONDS); 
	System.out.println("异步线程: " + thread.getName() + "返回的结果:" + s);
	System.out.println("====== 主线程执行结束 ========");
	long end = System.currentTimeMillis();
	System.out.println("========  共耗时 ============ " + (end-start) + "ms");
}

并发编程之 CompletableFuture_第4张图片

isDone()轮询,浪费无谓的 CPU 资源

public static void main(String[] args) throws Exception {
	long start = System.currentTimeMillis();
	FutureTask futureTask = new FutureTask<>(new MyThread());
	Thread thread = new Thread(futureTask);
	thread.start();
	System.out.println("====== 主线程执行结束 ========");
	// isDone 轮询等待
	while (true) {
		if (futureTask.isDone()) {
			String s = futureTask.get();
			System.out.println("异步线程处理返回结果===== " + s);
			break;
		} else {
			System.out.println("异步线程正在处理~~");
		}
	}
	long end = System.currentTimeMillis();
	System.out.println("========  共耗时 ============ " + (end-start) + "ms");
}

并发编程之 CompletableFuture_第5张图片

 1.3 Future 总结

通过上面的代码演示,可以得知,Future对于结果的获取不是很友好,只能通过阻塞或者轮询的方式得到任务的结果。显然,这是不够完美的,那么有没有一种更完美的解决方案呢?答案是有的,也就是 JDK1.8 中的 CompletableFuture,完美的解决了这些问题。

2. CompletableFuture

2.1 Future 的优化思路

① 对于简单的业务场景,使用 Futrue 完成 OK;

② 对于复杂的,高并发的业务场景,我们需要有回调通知,当异步线程执行完毕后,主动通知主线程,而不是通过 isDone()轮询的方式去一直等待,那样大大的占用了CPU 资源;

③ 异步任务(Future + 线程池配置使用);

④ 多任务前后依赖组合处理,比如下一个任务,需要前一个任务的结果进行支撑,两个任务可以独立处理,但是又有依赖关系;

⑤ 对计算速度选最快等(并行的异步任务,哪个先完成,返回哪个结果);

2.2 CompletableFuture 对 Future 的优化

Future的 get() 方法在计算完成之前会一直处于阻塞状态;

isDone()方法容易耗费 CPU 资源;

对于真正的异步处理,我们希望是可以通过传入回调函数,在 Future结束时自动调用该回调函数,这样我们就不用等待结果;

阻塞的方式和异步编程的设计理念相违背,而轮询的方式会耗费无畏的 CPU 资源,因此,JDK1.8 设计出 CompletableFuture。它提供了一种观察者模式类似的机制,可以让任务执行完后通知监听的一方。

2.3 CompletableFuture API

CompletableFuture 扩展了 Futrue的所有功能,同时提供了其他更加强大的功能。

 先来看一下类图:

并发编程之 CompletableFuture_第6张图片

并发编程之 CompletableFuture_第7张图片

  •  四大核心构造方法

在官方的 API 文档中,是不推荐直接使用无参构造方法来进行创建实例的。

并发编程之 CompletableFuture_第8张图片下面来看一下,推荐使用的四大核心构造方法:

共分为两类,一类是有返回值,一类是无返回值:

① runAsync  无返回值

② supplyAsync 有返回值

并发编程之 CompletableFuture_第9张图片

 Executor 参数说明: 如果没有指定 Executor 的方法,直接使用默认的 ForkJoinPool.commonPool()作为它的线程池执行异步代码;如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码。

代码演示:

无返回值

public static void main(String[] args) throws Exception {
	CompletableFuture completableFuture = CompletableFuture.runAsync(() -> {
		System.out.println(Thread.currentThread().getName());
		try {
			TimeUnit.SECONDS.sleep(1);
			System.out.println("task is over!!");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	});
	Void unused = completableFuture.get();
	System.out.println(unused);
}

并发编程之 CompletableFuture_第10张图片

public static void main(String[] args) throws Exception {
	// 自定义线程池
	ExecutorService pool = Executors.newFixedThreadPool(3);
	CompletableFuture completableFuture = CompletableFuture.runAsync(() -> {
		System.out.println(Thread.currentThread().getName());
		try {
			TimeUnit.SECONDS.sleep(1);
			System.out.println("task is over!!");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	},pool);
	Void unused = completableFuture.get();
	System.out.println(unused);
	// 使用完后进行关闭
	pool.shutdown();
}

并发编程之 CompletableFuture_第11张图片有返回值

public static void main(String[] args) throws Exception {
	// 自定义线程池
	ExecutorService pool = Executors.newFixedThreadPool(3);
	CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> {
		System.out.println(Thread.currentThread().getName());
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return "hello supplyAsync";
	},pool);
	String result = completableFuture.get();
	System.out.println(result);
	// 使用完后进行关闭
	pool.shutdown();

并发编程之 CompletableFuture_第12张图片

前面说了,CompletableFuture 是 Future的增强版,减少了阻塞和轮询,通过上面的代码,发现和 Future 并没有什么区别,那它是怎么做到的呢?来看下面的代码:

public static void main(String[] args) throws Exception {
	ExecutorService pool = Executors.newFixedThreadPool(3);
	CompletableFuture.supplyAsync(() -> {
		System.out.println(Thread.currentThread().getName());
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		// 产生一个 随机数
		int i = ThreadLocalRandom.current().nextInt(10);
		return i;
	},pool).whenComplete((v,e) -> {
		// v: 上一步的返回结果,e: 异常信息
		// 正常执行完毕,没有异常
		if (e == null) {
			System.out.println("异步线程执行完毕,进行回调======== " + v);
		}
	}).exceptionally(e -> {
		e.printStackTrace();
		System.out.println("异步线程出现异常了============ " + e.getCause() + "\t" + e.getMessage());
		return null;
	});
	System.out.println(Thread.currentThread().getName() + " 主线程执行处理其他业务去了 ===========");
	// 注意:使用默认的线程池 主线程不要 立刻结束,否则 CompletableFuture默认使用的线程池会立刻关闭
	TimeUnit.SECONDS.sleep(3);
	// 使用我们自己的线程池
	pool.shutdown();
}

通过上面的代码,可以很明显的看出 CompletableFuture灵活了很多。

异步任务结束时,会自动回调某个对象的方法;

主线程设置好回调后,不再关系异步任务的执行,异步任务之间可以顺序执行;

异步任务出错时,会自动回调某个对象的方法;

join 和 get 对比

两者都是获取异步线程的返回值,区别在于:

get 在编译期间会抛出异常,需要进行声明式抛出,允许被中断,抛出 InterruptedException 异常;

join 在编译期间不会抛出异常,在运行时进行处理,不允许被中断;

 allOf() 和 anyOf()

allOf: 接收多个 CompletableFuture 无返回值任务,当所有的任务执行结束后,返回一个新的 CompleatbleFuture 对象;

anyOf: 接收多个带有返回值的任务,当任何一个任务执行完成后,返回一个新的CompleatbleFuture 对象;

public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
	ExecutorService threadPool = Executors.newFixedThreadPool(3);
	CompletableFuture futureA = CompletableFuture.supplyAsync(() -> {
		try {
			TimeUnit.SECONDS.sleep(1L);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return 10;
	}, threadPool);
	CompletableFuture futureB = CompletableFuture.supplyAsync(() -> {
		try {
			TimeUnit.SECONDS.sleep(2L);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return 20;
	}, threadPool);
	System.out.println(CompletableFuture.anyOf(futureA, futureB).join());
	threadPool.shutdown();

 

2.4 经典案例实战

电商比价需求分析:

① 同一款产品,同时搜索出同款产品 在各大电商平台的售价;

② 同一款产品,同时搜索出本产品在同一个电商平台下,各个入驻卖家售价是多少;

 解决方案:

①中规中矩, 一个线程一步一步的去查;

②万箭齐发,异步线程,同时分开去查;

代码演示:

public class CompletableFutureTest {

	static List netMalls = Arrays.asList(
			new NetMall("JD"),
			new NetMall("taobao"),
			new NetMall("dangdang")
	);

	public static void main(String[] args) {
		long start = System.currentTimeMillis();
		List strings = getPrice(netMalls, "MySQL");
		strings.stream().forEach(item -> System.out.println(item));
		long end = System.currentTimeMillis();
		System.out.println(" ======== 共耗时 ===== " +  (end - start));
	}

	public static List getPrice(List malls, String productName) {
		return malls.stream()
				.map(item ->
						String.format("《" + productName + "》" + " in %s price is %.2f", item.getNetMallName(), item.calcPrice(productName)))
				.collect(Collectors.toList());
	}
}

class NetMall {

	@Getter
	private String netMallName;

	public NetMall(String netMallName) {
		this.netMallName = netMallName;
	}

	public double calcPrice(String productName) {
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return ThreadLocalRandom.current().nextDouble(5) *2 + productName.charAt(0);
	}
}

并发编程之 CompletableFuture_第13张图片

 第一种解决方案,满足了我们的需求,但是,不尽人意,此时,需要对功能进行性能优化,使用第二种解决方案进行优化:

public static void main(String[] args) {
	long start = System.currentTimeMillis();
	// 使用异步方式进行处理
	List strings = getPriceByAsync(netMalls, "MySQL");
	strings.stream().forEach(item -> System.out.println(item));
	long end = System.currentTimeMillis();
	System.out.println(" ======== 共耗时 ===== " +  (end - start));
}


/**
 * 异步处理
 * @param malls
 * @param productName
 * @return
 */
public static List getPriceByAsync(List malls, String productName) {
	return malls.stream()
			.map(item ->
					CompletableFuture.supplyAsync(
							() -> String.format("《" + productName + "》" + " in %s price is %.2f", item.getNetMallName(), item.calcPrice(productName))))
			.collect(Collectors.toList())
			.stream()
			.map(cf -> cf.join()).collect(Collectors.toList());
}

并发编程之 CompletableFuture_第14张图片

 通过优化,发现性能上升显著,由原来的 3s 到现在的 1s,非常棒!当然,此案例中只有 3 家电商平台,在现实的业务场景中,远远不止这么几家,当集合的数量越大的时候,性能提升的越明显。

2.5 CompletableFuture 常用方法

API内容较多,这里简单分为五组进行描述

① 获得结果和触发计算;

② 对计算结果进行处理;

③ 对计算结果进行消费;

④ 对计算速度进行选用;

⑤ 对计算结果进行合并;

 获得结果:get() / get(x,TimeUtil.xx) / join() / getNow("xxx")

	public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
		CompletableFuture supplyAsync = CompletableFuture.supplyAsync(() -> {
			try {
				TimeUnit.SECONDS.sleep(2);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return "123";
		});
//		// 线程阻塞,不见不散
//		String result = supplyAsync.get();
//		// 与 get 的区别在于不会在编译期间显示的声明异常;
//		String res = supplyAsync.join();
//		// 给定一个等待时间,在等待时间内,不返回结果,则抛出异常
//		String r = supplyAsync.get(1L, TimeUnit.SECONDS);
        TimeUnit.SECONDS.sleep(3L);
		// 在调用返回结果的时候,如果线程没有处理完,则使用给定的默认值,不会阻塞线程
		String defaultValue = supplyAsync.getNow("返回默认值");
	}
}

触发计算: completa(T value) 是否打断 get方法,立即获取括号值

	public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
		CompletableFuture supplyAsync = CompletableFuture.supplyAsync(() -> {
			try {
				TimeUnit.SECONDS.sleep(2);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return "123";
		});
//		TimeUnit.SECONDS.sleep(3L);
		// 如果异步线程没有执行完毕,则直接打断,获取给定的值
		System.out.println(supplyAsync.complete("aaa"));
		System.out.println(supplyAsync.join());
	}
}

对计算结果进行处理:thenApply / handle  计算结果存在依赖关系,两个线程串行化,区别在于两者对异常的处理稍有不同,下面通过代码来进行演示。

	public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
		ExecutorService threadPool = Executors.newFixedThreadPool(3);
		CompletableFuture future = CompletableFuture.supplyAsync(() -> {
			System.out.println(Thread.currentThread().getName() + "111");
			try {
				TimeUnit.SECONDS.sleep(1L);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return 1;
		}, threadPool).thenApply(f -> {
            // 故意抛出一个异常
            int i= f/0;
			System.out.println(Thread.currentThread().getName() + "222");
			return f + 2;
		}).thenApply(f -> {
			System.out.println(Thread.currentThread().getName() + "333");
			return f + 3;
		}).exceptionally(e -> {
			e.printStackTrace();
			System.out.println(e.getMessage());
			return null;
		});
		System.out.println(future.join());
		threadPool.shutdown();
	}
}

正常运行下

并发编程之 CompletableFuture_第15张图片

thenApply 对异常的处理 ,出现异常,直接终止

并发编程之 CompletableFuture_第16张图片

 handle 正常的运行下,和 thenApply 一样,主要来看一下异常的处理:

	public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
		ExecutorService threadPool = Executors.newFixedThreadPool(3);
		CompletableFuture future = CompletableFuture.supplyAsync(() -> {
			System.out.println(Thread.currentThread().getName() + "111");
			try {
				TimeUnit.SECONDS.sleep(1L);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return 1;
		}, threadPool).handle((f,e) -> {
			// 故意抛出一个异常
			int i= f/0;
			System.out.println(Thread.currentThread().getName() + "222");
			return f + 2;
		}).handle((f,e) -> {
			System.out.println(Thread.currentThread().getName() + "333");
			return f + 3;
		}).exceptionally(e -> {
			e.printStackTrace();
			System.out.println(e.getMessage());
			return null;
		});
		System.out.println(future.join());
		threadPool.shutdown();
	}
}

并发编程之 CompletableFuture_第17张图片

 通过异常来看,handle 有异常的时候,会跳过,继续往下执行。

对计算结果进行消费: thenAccept 接收任务的处理结果,并消费处理,无返回结果

	public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
		ExecutorService threadPool = Executors.newFixedThreadPool(3);
		CompletableFuture.supplyAsync(() -> {
			System.out.println(Thread.currentThread().getName() + "111");
			try {
				TimeUnit.SECONDS.sleep(1L);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return 1;
		}, threadPool).handle((f,e) -> {
			System.out.println(Thread.currentThread().getName() + "222");
			return f + 2;
		}).thenAccept(System.out::println);
		threadPool.shutdown();
	}
}

并发编程之 CompletableFuture_第18张图片

 thenRun / thenAccept / thenApply  三者的执行顺序

API 接口 执行顺序
thenRun 任务 A执行完毕执行 B,并且 B不需要 A的就结果
thenAccept 任务 A执行完毕执行 B,B需要 A的结果,但是任务 B无返回值
thenApply 任务 A执行完毕执行 B,B需要 A的结果,同时任务 B 有返回值

关于异步线程的线程池

1、没有传入自定义线程池, 都用默认线程池ForkJoinPool;

2、传入了一个自定义线程池,如果你执行第一个任务的时候,传入了一个自定义线程池:  

        调用thenRun方法执行第二个任务时,则第二个任务和第一个任务是共用同一个线程池。
        调用thenRunAsync执行第二个任务时,则第一个任务使用的是你自己传入的线程池,第二个任务使用的是ForkJoin线程池

3、备注

        有可能处理太快,系统优化切换原则,直接使用main线程处理
        其它如:thenAccept和thenAcceptAsync,thenApply和thenApplyAsync等,它们之间的区别也是同理

对计算速度选用: applyToEither  谁先完成,用谁的结果。

	public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
		ExecutorService threadPool = Executors.newFixedThreadPool(3);
		CompletableFuture A = CompletableFuture.supplyAsync(() -> {
			try {
				TimeUnit.SECONDS.sleep(1L);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return "A";
		}, threadPool);
		CompletableFuture B = CompletableFuture.supplyAsync(() -> {
			try {
				TimeUnit.SECONDS.sleep(2L);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return "B";
		}, threadPool);
		CompletableFuture future = A.applyToEither(B, f -> "Get is " + f);
		System.out.println(future.join());
		threadPool.shutdown();
	}
}

并发编程之 CompletableFuture_第19张图片

 对结果集合并: thenCombine  两个 completionStage任务都完成后,最终将江哥任务的结果一起交给 thenCombine 来处理,先完成的等待后完成的

	public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
		ExecutorService threadPool = Executors.newFixedThreadPool(3);
		CompletableFuture futureA = CompletableFuture.supplyAsync(() -> {
			try {
				TimeUnit.SECONDS.sleep(1L);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return 10;
		}, threadPool);
		CompletableFuture futureB = CompletableFuture.supplyAsync(() -> {
			try {
				TimeUnit.SECONDS.sleep(2L);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return 20;
		}, threadPool);
		CompletableFuture result = futureA.thenCombine(futureB, (x, y) -> x+y);
		System.out.println("两线程结果集合并结果为:" + result.join());
		threadPool.shutdown();
	}
} 
  

并发编程之 CompletableFuture_第20张图片

CompletableFuture 还有其他很多功能,小伙伴们可以去看一下其他相关的 API 接口,这里不做过多描述。

通过这篇文章,相信大家已经对 Java8 中的 CompletableFuture 有了深入掌握。

在日常的开发工作中,希望能合理运用到项目上,提升服务的性能!

你可能感兴趣的:(并发编程,java,并发,Future)