Java异步编程源码及原理解析
目录
1.Fork-Join
2.FutureTask
3.Async注解介绍
4.CompletableFuture
5.中间件+线程池
会当凌绝顶,一览众山小。
1.Fork-Join框架介绍
从一道面试题讲起:如何使用多线程实现归并排序?
public class Solution {
private static final ForkJoinPool pool = new ForkJoinPool(4); // N 核心处理器
// 多线程排序
private static void multiThreadSort(int[] a) {
SortTask task = new SortTask(a, 0, a.length - 1);
pool.invoke(task);
}
/**
* fork-join 框架实现归并排序
* 这里是分割
*/
private static class SortTask extends RecursiveTask<int[]> {
private int[] val;
private int start;
private int end;
private int middle;
public SortTask(int[] a, int start, int end) {
val = a;
this.start = start;
this.end = end;
middle = (start + end) / 2;//应写成start+((end-start)>>1)
if (merge == null) merge = new int[a.length];
}
@Override
protected int[] compute() {
List<SortTask> moreActions = getMoreActions();
if (moreActions != null && moreActions.size() > 0) {
// 执行所有子线程
for (SortTask t : moreActions) t.fork();
// 等待所有子线程完成
for (SortTask t : moreActions) t.join();
// 合并
merge(val, start, end, middle);
} else {
if (end - start == 1 && val[start] > val[end]) swap(start, end);
}
return val;
}
// 获取需要分割的其他桶任务
private List<SortTask> getMoreActions() {
if (end - start <= 1) return null;
List<SortTask> moreActions = new ArrayList<>();
moreActions.add(new SortTask(val, start, middle));
moreActions.add(new SortTask(val, middle + 1, end));
return moreActions;
}
private void swap(int i, int j) {
int a = val[i];
val[i] = val[j];
val[j] = a;
}
}
}
分别进行100万个数字排序,对各个算法都进行200次排序,可以看到,输出结果:
- 正常排序共耗时:35.43 秒
- 多线程排序共耗时:23.898 秒
但是如果我们把排序次数改为5次:
- 正常排序共耗时:1.616 秒
- 多线程排序共耗时:2.905 秒
使用Fork-Join和串行归并排序比较
- 在大量的作业的情况下,ForkJoin排序效率更高
- 在少量作业的情况下,多线程会带来一些额外的开销,比如线程创建等
Fork-Join框架介绍
- Java 1.7 引入了一种新的并发框架
- 主要用于实现“分而治之”的算法,特别是分治之后递归调用的函数
- 提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个相互独立的小任务,最终汇总每个小任务结果后得到大任务结果的框架
- 与其他ThreadPool共存
Fork-Join常用的用途
newWorkStealingPool抢占式线程池
-
newWorkStealingPool会更加所需的并行层次来动态创建和关闭线程。它同样会试图减少任务队列的大小,所以比较适于高负载的环境。同样也比较适用于当执行的任务会创建更多任务,如递归任务。
-
适合使用在很耗时的操作,但是newWorkStealingPool不是ThreadPoolExecutor的扩展,它是新的线程池类ForkJoinPool的扩展,但是都是在统一的一个Executors类中实现,由于能够合理的使用CPU进行对任务操作(并行操作),所以适合使用在很耗时的任务中。
newWorkStealingPool抢占式线程池核心源码分析
public class Executors {
/**
创建一个线程池,以维护足够的线程来支持给定的并行级别,可以使用多个队列减少争用。
并行度级别对应于活动参与的或可用的最大线程数参与任务处理。线程的实际数量可能动态
增长和收缩。newWorkStealingPool没有保证提交任务的顺序执行。
*
* @param parallelism 目标并行度级别
* @return the newly created thread pool
* @throws IllegalArgumentException if {@code parallelism <= 0}
* @since 1.8
*/
public static ExecutorService newWorkStealingPool(int parallelism) {
return new ForkJoinPool(parallelism,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
/**
* Creates a work-stealing thread pool using all
* {@link Runtime#availableProcessors available processors}
* as its target parallelism level.
* @return the newly created thread pool
* @see #newWorkStealingPool(int)
* @since 1.8
*/
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
}
Java 8 parallelStream并行流
并行流就是执行任务的时候分配给多个线程队列执行,但是如果某一个队列执行完成,其他队列还在执行,这个时候执行完成的队列就是空闲状态。
java8中的并行流,使用的是工作窃取模式,在一个队列的任务执行完成之后,他会去其他没有执行完成的任务队列里面窃取尾部的任务来执行。
简单地使用并行流:
import java.util.Arrays;
import java.util.List;
public class Solution {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
numbers.parallelStream().forEach(System.out::println);//结果是乱序的
}
}
使用并行流并打印线程名:
可以看到并行流使用Main线程和各个ForkJoinPool的工作线程并行执行。
Java 8 CompleteableFuture
后文会讲
Fork-Join的原理
- 1、ForkJoinPool中的所有的工作线程均有一个自己的工作队列WorkQueue
- 双端队列(Deque)
- 从队头取任务
- 线程私有,不共享
- 2、ForkJoinTask中fork的子任务,将放入运行该任务的工作线程的队头
- 工作线程以LIFO的顺序来处理它队列中的任务
- 3、为了最大化CPU利用率,空闲的线程将从其他线程的队列中“窃取”任务来执行
- 从工作队列的队尾“窃取”任务,以减少竞争
- 任务的“窃取”是以FIFO顺序进行的,因为先放入的任务往往表示更大的工作量
- 支持“窃取”线程进行进一步的递归分解
- 3、WorkQueue双端队列最小化任务“窃取”的竞争
- push()/pop()仅在其所有者工作线程中调用,这些操作都是通过CAS来实现的,是Wait-free的
- poll() 则由其他工作线程来调用“窃取”任务,可能不是wait-free
任务窃取流程如下:
当WorkQueue为空时抢任务:
一张图表示工作窃取模型:
Fork-Join的核心源码
参考我之前写的PPT:《Fork/join框架与CompleteableFuture源码解析》
后面再写
2.FutureTask
FutureTask提供了对Future的基本实现,可以调用方法去开始和取消一个计算,可以查询计算是否完成并且获取计算结果。
简单地介绍Future接口
Future的类图:
RunnableFuture
这个接口同时继承Future接口和Runnable接口,在成功执行run()方法后,可以通过Future访问执行结果。这个接口都实现类是FutureTask,一个可取消的异步计算,这个类提供了Future的基本实现,后面我们的demo也是用这个类实现,它实现了启动和取消一个计算,查询这个计算是否已完成,恢复计算结果。计算的结果只能在计算已经完成的情况下恢复。如果计算没有完成,get方法会阻塞,一旦计算完成,这个计算将不能被重启和取消,除非调用runAndReset方法。
FutureTask能用来包装一个Callable或Runnable对象,因为它实现了Runnable接口,而且它能被传递到Executor进行执行。为了提供单例类,这个类在创建自定义的工作类时提供了protected构造函数。
SchedualFuture
这个接口表示一个延时的行为可以被取消。通常一个安排好的future是定时任务SchedualedExecutorService的结果
CompleteFuture
一个Future类是显示的完成,而且能被用作一个完成等级,通过它的完成触发支持的依赖函数和行为。当两个或多个线程要执行完成或取消操作时,只有一个能够成功。
ForkJoinTask
基于任务的抽象类,可以通过ForkJoinPool来执行。一个ForkJoinTask是类似于线程实体,但是相对于线程实体是轻量级的。大量的任务和子任务会被ForkJoinPool池中的真实线程挂起来,以某些使用限制为代价。
简单地使用FutureTask
配合线程池使用
public class FutureTest {
//(1)线程池
private final static ThreadPoolExecutor executorService = new ThreadPoolExecutor(3, 3, 1L, TimeUnit.MINUTES,
new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.DiscardPolicy());
public static void main(String[] args) throws ExecutionException, InterruptedException {
Future future1 = executorService.submit(() -> System.out.println("start runable one " + Thread.currentThread().getName()));
Future future2 = executorService.submit(() -> System.out.println("start runable two " + Thread.currentThread().getName()));
Future future3 = executorService.submit(() -> System.out.println("start runable three " + Thread.currentThread().getName()));
Future<Integer> future4 = executorService.submit(() -> {
System.out.println("start runable four " + Thread.currentThread().getName());
return 1;
});
System.out.println("task one res:" + future1.get() + Thread.currentThread().getName());//(5)等待任务one执行完毕
System.out.println("task two res:" + future2.get() + Thread.currentThread().getName());//(6)等待任务two执行完毕
System.out.println("task three res:" + future3.get() + Thread.currentThread().getName());// (7)等待任务three执行完毕
System.out.println("task four res:" + future4.get() + Thread.currentThread().getName());// (8)等待任务four执行完毕
executorService.shutdown();//(9)关闭线程池,阻塞直到所有任务执行完毕
}
}
运行结果:
start runable one pool-1-thread-1
start runable two pool-1-thread-2
start runable three pool-1-thread-3
task one res:nullmain
start runable four pool-1-thread-1
task two res:nullmain
task three res:nullmain
task four res:1main
单独使用
FutureTask的核心方法如下:
public class FutureTask<V> implements RunnableFuture<V> {
// 获取结果,如果计算未完成,当前线程会被阻塞
public V get() throws InterruptedException, ExecutionException {...}
// 获取结果,如果计算未完成,当前线程会被阻塞,但是有时间限制
// 如果超时还未完成,抛出 TimeoutException
public V get(long timeout, TimeUnit unit){...}
// 进行计算,运行 Callable 的 call 方法,然后对结果进行赋值
public void run(){...}
// 进行计算,运行 Callable 的 call 方法,然后将 state 重置为 NEW
// 不对结果进行赋值,可以重复多次运行,只有子类可以调用,用来进行扩展,例如 ScheduledFutureTask
protected void runAndReset(){...}
// 取消任务,参数 mayInterruptIfRunning 表示取消过程中是否中断当前线程
public boolean cancel(boolean mayInterruptIfRunning){...}
// 查看任务是否被取消
public boolean isCancelled(){...}
// 查看任务是否完成
public boolean isDone(){...}
}
具体的使用方式请百度看其他的文章
优缺点
优点
- 1、可配合线程池使用,也可以单独创建,适合使用在经典的线程池中。
- 2、适合在JDK1.5~1.7的版本中,因为CompletableFuturn在JDK1.8的版本中才出现。
缺点
- 1、用法比CompletableFuture复杂。
- 2、执行效率不如CompletableFuture。
CompletableFuture是何方神圣?后文说明。
请忘掉FutureTask!
3.Async注解
对于异步方法调用,从Spring3开始提供了@Async注解,该注解可以被标在方法上,以便异步地调用该方法。调用者将在调用时立即返回,方法的实际执行将提交给Spring TaskExecutor的任务中,由指定的线程池中的线程执行。
Spring 已经实现的线程池
- SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,默认每次调用都会创建一个新的线程。
- SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。
- ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类。
- SimpleThreadPoolTaskExecutor:是Quartz的SimpleThreadPool的类。线程池同时被quartz和非quartz使用,才需要使用此类。
- ThreadPoolTaskExecutor :最常使用,推荐。其实质是对java.util.concurrent.ThreadPoolExecutor的包装。
异步的方法
- 最简单的异步调用,返回值为void
- 带参数的异步调用,异步方法可以传入参数
- 存在返回值,常调用返回Future
使用@Async的步骤
1、Java配置
@Configuration
@EnableAsync
public class SpringAsyncConfig { ... }
2、使用@Async注解标注方法
//基于@Async无返回值调用
@Async
public void asyncMethodWithVoidReturnType() {...}
//基于@Async返回值的调用
@Async
public Future<T> asyncMethodWithReturnType() {...}
是不是感觉太复杂了?
优缺点
- 1、依赖于Spring(侵入式)。
- 2、用法比较复杂。
4.CompletableFuture
先介绍Java8的函数式编程
四种常用的函数介绍
- (1)Function<T, R> => R apply(T t) ———— 接受一个T类型的参数,返回R类型结果。
Function<Integer, String> function = (x) -> "result: " + x;
System.out.println(function.apply(6));
- (2)Consumer => void accept(T t) ———— 接受一个T类型的参数,无返回。
Consumer<String> consumer = (x) -> System.out.println("consumer: " + x);
consumer.accept("Hello");
//见过System.out::println没?
- (3)Predicate => boolean test(T t) ———— 接受一个T类型的参数,返回布尔值。
Predicate<String> predicate = (x) -> x.length() > 0;
System.out.println(predicate.test("String"));
- (4)Supplier => T get() ———— 无输入参数,返回T类型的一个结果。
Supplier<String> supplier = () -> "Test supplier";
System.out.println(supplier.get());
在CompleteableFuture中,有大量这样类型的传参
CompletableFuture的核心API介绍
supplyAsync / runAsync
supplyAsync表示创建带返回值的异步任务的,相当于ExecutorService submit(Callable task) 方法,runAsync表示创建无返回值的异步任务,相当于ExecutorService submit(Runnable task)方法,这两方法的效果跟submit是一样的。
supplyAsync的用法
// 创建异步执行任务,有返回值
CompletableFuture<Double> future1 = CompletableFuture.supplyAsync(() -> {
System.out.println("future1:" + Thread.currentThread().getName());
return 3.14159;
});
//等待子任务执行完成
Double res = future1.get();
System.out.println("run result->" + res + " 当前线程:" + Thread.currentThread().getName());
打印结果:
future1:ForkJoinPool.commonPool-worker-1
run result->3.14159 当前线程:main
runAsync的用法
// 创建异步执行任务,无返回值
CompletableFuture.runAsync(() -> {
System.out.println("future2:" + Thread.currentThread().getName());
System.out.println("hello world");
});
打印结果:
future2:ForkJoinPool.commonPool-worker-1
hello world
这两方法各有一个重载版本,可以指定执行异步任务的Executor实现,如果不指定,默认使用ForkJoinPool.commonPool(),如果机器是单核的,则默认使用ThreadPerTaskExecutor,该类是一个内部类,每次执行execute都会创建一个新线程。
ForkJoinPool pool = new ForkJoinPool();
ExecutorService executorService = Executors.newSingleThreadExecutor();
CompletableFuture.supplyAsync(() -> 3.1415926, pool);
CompletableFuture.runAsync(() -> System.out.println("hello"), executorService);
异步回调thenApply / thenApplyAsync
thenApply 表示某个任务执行完成后执行的动作,即回调方法,会将该任务的执行结果即方法返回值作为入参传递到回调方法中。
thenApplyAsync与thenApply的区别在于,前者是将job2提交到线程池中异步执行,实际执行job2的线程可能是另外一个线程,后者是由执行job1的线程立即执行job2,即两个job都是同一个线程执行的。
ForkJoinPool pool = new ForkJoinPool();
// 创建异步执行任务:
CompletableFuture<Double> future1 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName());
return 3.1415926;
}, pool);
//future1关联的异步任务的返回值作为方法入参,传入到thenApply的方法中
//thenApply这里实际创建了一个新的CompletableFuture实例
CompletableFuture<String> future2 = future1.thenApply((result) -> {
System.out.println(Thread.currentThread().getName());
return "半径为3的圆的周长:" + 3 * 2 * result;
});
CompletableFuture<Double> future3 = future1.thenApplyAsync((result) -> {
System.out.println(Thread.currentThread().getName());
return 3 * 3 * result;
});
//等待子任务执行完成
System.out.println(Thread.currentThread().getName() + " " + future2.get());//这里不考虑计算结果精度,暂时不用BigDecimal
System.out.println(Thread.currentThread().getName() + " 面积:" + future3.get());
打印结果:
ForkJoinPool-1-worker-1
main
main run result->半径为3的圆的周长:18.849555600000002
thenAccept / thenRun
thenAccept 同 thenApply 接收上一个任务的返回值作为参数,但是无返回值;thenRun 的方法没有入参,也没有返回值。
ForkJoinPool pool = new ForkJoinPool();
// 创建异步执行任务:
CompletableFuture<Double> future1 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName());
return 3.1415926;
}, pool);
//future1关联的异步任务的返回值作为方法入参,传入到thenApply的方法中
CompletableFuture<Void> future2 = future1.thenApply((result) -> {
System.out.println(Thread.currentThread().getName() + " 旧的res:" + result);
return result + 1;
//接收上一个任务的执行结果作为入参,但是没有返回值
}).thenAccept(result2 -> {
System.out.println(Thread.currentThread().getName() + " 新的res:" + result2);
}).thenRun(() -> { //无入参,也没有返回值
System.out.println(Thread.currentThread().getName() + " hello ");
});
//等待子任务执行完成
System.out.println("run result->" + future1.get());
//future2 等待最后一个thenRun执行完成
System.out.println("run result->" + future2.get());
打印结果:
ForkJoinPool-1-worker-1
main 旧的res:3.1415926
main 新的res:4.1415926
main hello
run result->3.1415926
run result->null
exceptionally异常处理
exceptionally方法指定某个任务执行异常时执行的回调方法,会将抛出异常作为参数传递到回调方法中,如果该任务正常执行则会exceptionally方法返回的CompletionStage的result就是该任务正常执行的结果。
double pi = 3.1415926;
ForkJoinPool pool = new ForkJoinPool();
// 创建异步执行任务:
CompletableFuture<Double> future1 = CompletableFuture.supplyAsync(() -> {
System.out.println("future1当前线程名:" + Thread.currentThread().getName());
System.out.println(1 / 0);
return pi;
}, pool);
//future1执行异常时,将抛出的异常作为入参传递给回调方法
CompletableFuture<Double> future2 = future1.exceptionally(e -> {
System.out.println("future2当前线程名:" + Thread.currentThread().getName());
e.printStackTrace();
return pi + 1;
});
//future1正常执行时执行的逻辑,如果执行异常则不调用此逻辑
future1.thenAccept((param) -> {
System.out.println("future3当前线程名:" + Thread.currentThread().getName());
System.out.println("半径为3的圆的周长" + param * 3 * 2);
});
//等待子任务执行完成,此处无论是job2和job3都可以实现job2退出,主线程才退出,如果是future1,则主线程不会等待job2执行完成自动退出了
//future2.get时,没有异常,但是依然有返回值,就是cf的返回值
System.out.println("run result->" + future2.get());
当出异常时,打印结果:
future1当前线程名:ForkJoinPool-1-worker-1
future2当前线程名:main
run result->4.1415926
java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
无异常时,打印结果:
future1当前线程名:ForkJoinPool-1-worker-1
future3当前线程名:main
半径为3的圆的周长18.849555600000002
run result->3.1415926
其他API
此外,还有whenComplete、handle、组合处理thenCombine / thenAcceptBoth / runAfterBoth、applyToEither / acceptEither / runAfterEither、thenCompose等很好用的API,在此不做详细说明。
allOf / anyOf
allOf返回的CompletableFuture是多个任务都执行完成后才会执行,只有有一个任务执行异常,则返回的CompletableFuture执行get方法时会抛出异常,如果都是正常执行,则get返回null。
anyOf返回的CompletableFuture是多个任务只要其中一个执行完成就会执行,其get返回的是已经执行完成的任务的执行结果,如果该任务执行异常,则抛出异常。
// 创建异步执行任务:
CompletableFuture<Double> future1 = CompletableFuture.supplyAsync(() -> {
System.out.println("future1当前线程名:" + Thread.currentThread().getName());
return 0.02;
});
CompletableFuture<Double> future2 = CompletableFuture.supplyAsync(() -> {
System.out.println("future2当前线程名:" + Thread.currentThread().getName());
return 0.03;
});
CompletableFuture<Double> future3 = CompletableFuture.supplyAsync(() -> {
System.out.println("future3当前线程名:" + Thread.currentThread().getName());
System.out.println(1 / 0);
return 0.04;
});
CompletableFuture<Double> future4 = CompletableFuture.supplyAsync(() -> {
System.out.println("future4当前线程名:" + Thread.currentThread().getName());
return 0.05;
});
//allof等待所有任务执行完成才执行future5,如果有一个任务异常终止,则cf4.get时会抛出异常,都是正常执行,cf4.get返回null
CompletableFuture.allOf(future1, future2, future3, future4).whenComplete((a, b) -> {
if (b != null) {
b.printStackTrace();
} else {
System.out.println("allof->" + a);
}
});
//anyOf是只有一个任务执行完成,无论是正常执行或者执行异常,都会执行cf4,cf4.get的结果就是已执行完成的任务的执行结果
CompletableFuture.anyOf(future1, future2, future3, future4).whenComplete((a, b) -> {
if (b != null) {
b.printStackTrace();
} else {
System.out.println("anyOf->" + a);
}
});
打印结果:
future1当前线程名:ForkJoinPool.commonPool-worker-1
future2当前线程名:ForkJoinPool.commonPool-worker-1
future3当前线程名:ForkJoinPool.commonPool-worker-2
future4当前线程名:ForkJoinPool.commonPool-worker-2
anyOf->0.02
java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
CompletableFuture的底层原理
CompletableFuture的核心构造:ForkJoinPool
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
/**
默认执行器—— ForkJoinPool.commonPool() 除非它不能支持并行性。
*/
private static final Executor asyncPool = useCommonPool ? ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();
/**
返回一个新的 CompletableFuture,它由在 ForkJoinPoolcommonPool() 中运行的任务异步完成,其值是通过调用给定的供应商获得的。
* @param supplier a function returning the value to be used
* to complete the returned CompletableFuture
* @param <U> the function's return type
* @return the new CompletableFuture
*/
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
return asyncSupplyStage(asyncPool, supplier);
}
static <U> CompletableFuture<U> asyncSupplyStage(Executor e, Supplier<U> f) {
if (f == null) throw new NullPointerException();
CompletableFuture<U> d = new CompletableFuture<U>();
//Supplier转换为ForkJoinTask
e.execute(new AsyncSupply<U>(d, f));
return d;
}
}
CompleteableFuture到ForkJoinTask转换流程如下:
ForkJoinTask的类图如下:
CompleteableFuture类的接口层面与内部实现:
CompletableFuture任务的链式执行过程分析
以 supplyAsync().thenApply().thenRun()链式代码为例
第1步:CompletableFuture future1=CompletableFuture.supplyAsync()
ForkJoinPool执行一个ForkJoinTask类型的任务,即AsyncSupply。输入是Supply,输出结果存放在CompletableFuture中。
第2步:CompletableFuture future2=future1.thenApply()
public <U> CompletableFuture<U> thenApply(Function<? super T, ? extends U> fn) {
return uniApplyStage(null, fn);
}
private CompletableFuture<Void> uniAcceptStage(Executor e, Consumer<? super T> f) {
if (f == null) throw new NullPointerException();
// 新创建了一个CompletableFuture
CompletableFuture<Void> d = new CompletableFuture<Void>();
// executor 传入的是null, d.uniaccept判断驱动thenAccept的CompletableFuture是否运行完/是否运行
if (e != null || !d.uniAccept(this, f, null)) {
// 用新创建的CompletableFuture和驱动thenAccept的CompletableFuture构建一个UniAccept
UniAccept<T> c = new UniAccept<T>(e, d, this, f);
// 放入栈中,此栈是第一步执行完,返回的CompletableFuture
push(c);
c.tryFire(SYNC);
}
return d;
}
第3步:CompletableFuture future3=future2.thenRun()
public CompletableFuture<Void> thenRun(Runnable action) {
return uniRunStage(null, action);
}
private CompletableFuture<Void> uniRunStage(Executor e, Runnable f) {
if (f == null) throw new NullPointerException();
CompletableFuture<Void> d = new CompletableFuture<Void>();
if (e != null || !d.uniRun(this, f, null)) {
CompletableFuture.UniRun<T> c = new CompletableFuture.UniRun<T>(e, d, this, f);
push(c);
c.tryFire(SYNC);
}
return d;
}
第3步和第2步的过程类似,构建了一个 UniRun 对象,这个对象 被压入第2步CompletableFuture所在的栈中。当第2步的任务执行完成时,从自己的栈中弹出UniRun对象并执行。
thenApply与thenApplyAsync的区别
- (1)如果前置任务没有完成,即a.result=null,则上面的uniApply会返回false,此时thenApply也会走到thenApplyAsync的逻辑里面,生成UniApply对象入栈;
- (2)只有在前置任务已经完成的情况下,thenApply才会立即执行,不会入栈,再出栈,此时thenApply和thenApplyAsync才有区别。
- 同理,thenRun与thenRunAsync、thenAccept与thenAcceptAsync的区别与此类似。
核心源码如下:
public <U> CompletableFuture<U> thenApply(Function<? super T, ? extends U> fn) {
return uniApplyStage(null, fn);
}
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T, ? extends U> fn) {
return uniApplyStage(asyncPool, fn);
}
private <V> CompletableFuture<V> uniApplyStage(Executor e, Function<? super T, ? extends V> f) {
if (f == null) throw new NullPointerException();
CompletableFuture<V> d = new CompletableFuture<V>();
if (e != null || !d.uniApply(this, f, null)) {
UniApply<T, V> c = new UniApply<T, V>(e, d, this, f);
push(c);
c.tryFire(SYNC);
}
return d;
}
//根据依赖的上一个阶段a是否完成,看要不要立即安排当前任务执行
//返回true表示已经同步完成执行了当前任务。为false表示依赖的阶段a还没完成,需要等待,或者已经安排异步执行(如果是异步任务的话)
final <S> boolean uniApply(CompletableFuture<S> a, Function<? super S, ? extends T> f, UniApply<S, T> c) {
Object r;
Throwable x;
if (a == null || (r = a.result) == null || f == null) return false;//表示依赖的阶段a还没完成,还不能执行当前阶段
tryComplete:
if (result == null) { //依赖的阶段a已经完成,当前阶段还没完成
if (r instanceof AltResult) {
//如果依赖的阶段a是异常结束,那么当前阶段也异常结束
if ((x = ((AltResult) r).ex) != null) {
completeThrowable(x, r);
break tryComplete;
}
r = null;
}
//到这里表示依赖的阶段a是正常结束
try {
if (c != null && !c.claim()) return false; //只有在c不为空,并且不能被执行或者已经安排异步执行才会返回false
//拿到已经完成的依赖阶段a的结果,执行同步执行当前任务,并把结果设置到当前CompletableFuture阶段
@SuppressWarnings("unchecked") S s = (S) r;
completeValue(f.apply(s));
} catch (Throwable ex) {
//异常完成的处理
completeThrowable(ex);
}
}
return true;
}
//通过自定义TAG,标记任务正在被执行,保证任务只会被执行一次。
//该方法只会在不能被执行或者已经安排异步执行才会返回false
final boolean claim() {
Executor e = executor;
//解锁成功,表示可以执行了
if (compareAndSetForkJoinTaskTag((short) 0, (short) 1)) {
if (e == null) return true; //需要被安排同步执行,立即返回true
executor = null; // disable 赋值GC
e.execute(this); //否则立即安排异步执行
}
return false;
}
CompletableFuture任务的网状执行
有向无环图
如果任务只是链式执行,便不需要在每个CompletableFuture里面设1个栈了,用1个指针使所有任务组成链表即可。
但实际上,任务不只是链式执行,而是网状执行,组成1张图。
- (1)在每个任务的返回值里面,存储了依赖它的接下来要执行的任务。
- (2)任务2、任务3的CompletableFuture里面,都存储了任务5,那么任务5是不是会被触发两次,执行两次呢?任务5的确会被触发2次,但它会判断任务2、任务3的结果是不是都完成,如果只完成其中一个,它就不会执行。
- (3)任务7存在于任务4、任务5、任务6的CompletableFuture的栈里面,因此会被触发三次。但它只会执行一次,只要其中1个任务执行完成,就可以执行任务7了。
- (4)正因为有And和Or两种不同的关系,因此对应BiApply和OrApply两个对象,这两个对象的构造函数几乎一样,只是在内部执行的时候,一个是And的逻辑,一个是Or的逻辑。
正因为有And和Or 两种不同的关系,因此对应BiApply和OrApply两个对象,这两个对象的构造函数几乎一样,只是在内部执行的时候,一个是And的逻辑,一个是Or的逻辑。
static final class OrApply<T, U extends T, V> extends CompletableFuture.BiCompletion<T, U, V> {
// ...
OrApply(Executor executor, CompletableFuture<V> dep, CompletableFuture<T> src, CompletableFuture<U> snd, Function<? super T, ? extends V> fn) {
super(executor, dep, src, snd);
this.fn = fn;
}
// ...
}
static final class BiApply<T, U, V> extends CompletableFuture.BiCompletion<T, U, V> {
// ...
BiApply(Executor executor, CompletableFuture<V> dep, CompletableFuture<T> src, CompletableFuture<U> snd, BiFunction<? super T, ? super U, ? extends V> fn) {
super(executor, dep, src, snd);
this.fn = fn;
}
// ...
}
多元操作转换为二元操作
任何一个多元操作,都能被转换为多个二元操作的叠加。假如任务1And任务2And任务3=任务4,那么它可以被转换为右边的形式。新建了一个And任务,这个And任务和任务3再作为参数,构造任务4。
Or的关系,与此类似。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tGePldrW-1630666588875)(多元操作转换为二元操作.png)]
allOf内部的计算图分析
allOf核心源码
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs) {
return andTree(cfs, 0, cfs.length - 1);
}
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs) {
return orTree(cfs, 0, cfs.length - 1);
}
static CompletableFuture<Void> andTree(CompletableFuture<?>[] cfs, int lo, int hi) {
CompletableFuture<Void> d = new CompletableFuture<Void>();
if (lo > hi) // empty
d.result = NIL;
else {
CompletableFuture<?> a, b;
int mid = (lo + hi) >>> 1;
if ((a = (lo == mid ? cfs[lo] : andTree(cfs, lo, mid))) == null ||
(b = (lo == hi ? a : (hi == mid + 1) ? cfs[hi] : andTree(cfs, mid + 1, hi))) == null
throw new NullPointerException();
if (!d.biRelay(a, b)) {
CompletableFuture.BiRelay<?, ?> c = new CompletableFuture.BiRelay<>(d, a, b);
a.bipush(b, c);//把c压入a,b所在的栈中,因为c要等a,b都执行完成之后才能执行
c.tryFire(SYNC);
}
}
return d;
}
final void bipush(CompletableFuture<?> b, BiCompletion<?, ?, ?> c) {
if (c != null) {
Object r;
while ((r = result) == null && !tryPushStack(c)) //c压入a的栈
lazySetNext(c, null); // clear on failure
if (b != null && b != this && b.result == null) {
Completion q = (r != null) ? c : new CoCompletion(c);
while (b.result == null && !b.tryPushStack(q)) //c压入b的栈
lazySetNext(q, null); // clear on failure
}
}
}
allOf内部的运作过程
方块表示任务,椭圆表示任务的执行结果。假设allof的参数传入了future1、future2、future3、future4,则对应四个原始任务。生成BiRelay1、BiRelay2任务,分别压入future1/future2、future3/future4的栈中。无论future1或future2完成,都会触发BiRelay1;无论future3或future4完成,都会触发BiRelay2;生成BiRelay3任务,压入future5/future6的栈中,无论future5或future6完成,都会触发BiRelay3任务。
BiRelay只是一个中转任务,它本身没有任务代码,只是参照输入的两个future是否完成。如果完成,就从自己的栈中弹出依赖它的BiRelay任务,然后执行。
优缺点
作为Java8的新特性,CompletableFuture广受好评,下面简单地做下总结。
优点
- 1、充分利用多核CPU并行执行逻辑,效率高。
- 2、API简单,还支持链式调用、异常处理等操作。
缺点
- 1、JDK1.7及以前的项目不可用。
- 2、不好控制CPU、内存等资源对使用情况,可能会导致系统宕机。
5.中间件+线程池
在不使用@Async、FutureTask、CompletableFuture的前提下实现异步执行任务。
核心思路
借助中间件存储任务队列信息(如Redis),使用定时任务从队列中拉取任务,使用线程池执行任务。
实现逻辑
- 1、创建Redis任务队列、用户任务Map(非必须)。
- 2、创建异步任务对象并设置属性。
- 3、往任务队列里push任务,若要控制每个用户的任务提交量,则还需要给用户任务Map中put一个<用户id,用户任务队列>的Map,该过程耗时时间不长。
- 4、启动定时任务执行任务队列的任务,成功与否都将任务从队列中移除,若出现异常,往钉钉群里发送邮件警告。
具体实现流程伪代码
注意:以下是伪代码,无法正常执行,只展示核心逻辑,源代码已经过验证没问题。
1、创建任务对象
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
class TaskPOJO {
//用户id
private Integer userId;
//任务id
private String taskId;
//模拟任务
private String task = "hello world!";
}
2、创建Redis任务队列、用户任务Map(非必须),编写提交异步任务的方法(建议写在Service)
public class XXXController {
//单个用户最大任务数量
private final static int MAX_TASK_NUM = 5;
//定义Redis中任务队列名
private final static String TASK_QUEUE_NAME = "";
//定义Redis中用户任务队列名
private final static String USER_TASK_MAP_NAME = "";
// 任务队列
private RQueue<TaskPOJO> taskQueue;
// 用户任务队列
private RMapCache<Long, Set<String>> userCachedCommitTaskMap;
@Resource
private RedissonClient redissonClient;
@PostConstruct
public void init() {
taskQueue = redissonClient.getQueue(TASK_QUEUE_NAME);
userCachedCommitTaskMap = redissonClient.getQueue(USER_TASK_MAP_NAME);
}
/**
* 异步提交任务
*/
public void asyncTask() {
Integer userId = 1;
String taskId = "001";
TaskPOJO task = new TaskPOJO(userId, taskId, "测试001");
Set<String> taskIds = userCachedCommitTaskMap.get(userId);
if (CollectionUtils.isNotEmpty(taskIds) && taskIds.size() > MAX_TASK_NUM) {
//报个自定义异常,不允许提交
}
taskQueue.add(task);
taskIds.add(taskId);
userCachedCommitTaskMap.set(userId, taskIds);
}
}
3、使用定时任务+线程池自动从Redis队列中拉取任务执行
@Slf4j
@Component
public class TaskExecuteJob {
//分布式锁名
private final static String R_LOCK_NAME = "";
@Resource
private RedissonClient redissonClient;
//使用线程池,此处用《阿里巴巴java开发手册》不建议的写法,实际工作中可以根据实际情况配置线程池参数
private final ExecutorService executorService = Executors.newFixedThreadPool(60);
/**
* 每N秒执行一次
*/
@Scheduled(cron = "0/5 * * * * ?")
public void bathSendMailScheduled() {
RLock lock = redissonClient.getLock(R_LOCK_NAME);
if (lock.isLocked()) {
return;
}
RQueue<TaskPOJO> redisQueue = redissonClient.getQueue(TASK_QUEUE_NAME);
if (CollectionUtils.isEmpty(redisQueue)) {
return;
}
TaskPOJO taskPOJO;
try {
lock.lock(15, TimeUnit.MINUTES);//分布式锁自动释放时间不能随便设置,在此不考虑存在的问题
taskPOJO = redisQueue.poll();
String task = taskPOJO.getTask();
//执行具体逻辑
executorService.execute(() -> System.out.println(task));
} catch (Exception e) {
//发送异常通知
} finally {
//从队列中移除任务id
redisQueue.remove(taskPOJO.getTaskId());
//同时删除用户任务队列中的taskId,此处省略代码
lock.unlock();
}
}
}
优缺点
用这种方式执行异步任务缺点很明显,但是也有它的优势,以下简单地对它的优缺点做下总结。
- 优点
- 1、使用Redis队列存储异步任务,若异步任务需要被撤回、修改属性,直接从队列中修改未执行的任务即可。
- 2、借助线程池执行,线程池参数可控制。可根据任务量动态对硬件、配置参数进行动态扩容、缩容,避免大量的异步任务在同一个服务或同一台机器上执行导致宕机。
- 缺点
- 1、使用定时任务拉取任务,会存在停顿时间,降低执行效率。
- 2、高可用问题,Redis挂掉、机器挂掉会引起一系列问题。
- 3、对中间件依赖比较高。
6.加餐:Java响应式编程Reactive
响应式编程是一种编程概念,当前Java中没有标准统一的响应式API实现。当前有大量的库提供不同的实现,以及工具运行响应式编程。
同步非阻塞
响应式编程是观察者模式的扩展,以RxJava中的实现为例:
import java.util.Observable;
public class Main {
public static void main(String[] args) {
//可观察对象
MyObservable observable = new MyObservable();
//添加观察者
for (int i = 0; i < 10; i++) {
int finalI = i;
observable.addObserver((o, arg) -> System.out.println("观察者" + finalI + "处理事件:" + arg.toString()));
}
//发布事件通知观察者
observable.setChanged();
//如果该对象发生了变化,由所示hasChanged方法,则通知其所有观察者,并调用clearChanged方法来指示该对象不再改变。
observable.notifyObservers("事件" + Thread.currentThread().getName());
}
static class MyObservable extends Observable {
@Override
public void setChanged() {
//将此Observable对象标记为已更改; hasChanged方法现在将返回true 。
super.setChanged();
}
}
}
执行结果:
观察者9处理事件:事件main
观察者8处理事件:事件main
观察者7处理事件:事件main
观察者6处理事件:事件main
观察者5处理事件:事件main
观察者4处理事件:事件main
观察者3处理事件:事件main
观察者2处理事件:事件main
观察者1处理事件:事件main
观察者0处理事件:事件main
可以看出,代码的执行顺序并非按照我们的代码顺序执行,而是反过来,都是在main线程中同步执行。这种方式可以称为同步非阻塞的响应式编程。
异步非阻塞
既然有同步式的非阻塞,那就有异步非阻塞的响应式编程,在Java中的Swing就是一个很好的例子。
import javax.swing.*;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class Main {
public static void main(String[] args) {
JFrame jFrame = new JFrame();
jFrame.setVisible(true);
jFrame.setBounds(200, 200, 400, 400);
jFrame.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
//在组件上单击(按下和释放)鼠标按钮时调用。
super.mouseClicked(e);
System.out.println("鼠标点击事件" + Thread.currentThread().getName());
}
});
jFrame.addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
//当组件获得键盘焦点时调用
super.focusGained(e);
System.out.println("焦点事件" + Thread.currentThread().getName());
}
});
}
}
执行结果:
焦点事件AWT-EventQueue-0
焦点事件AWT-EventQueue-0
鼠标点击事件AWT-EventQueue-0
焦点事件AWT-EventQueue-0
鼠标点击事件AWT-EventQueue-0
在swing的事件响应处理并不是在main线程里面进行处理的,鼠标点击和焦点事件处理都是在一个叫AWT-EventQueue-0的线程中进行处理,可以看见这种异步处理的方式有别于上面的观察者模式。
这里对于事件的响应更加类似于一种对事件进行拉取的方式,点击窗体,发现打印鼠标事件是有延迟的,原因就是这里对于事件的获取是采用另起一个线程轮询策略,监听到对应的事件之后委托给对应的事件处理器(回调函数)进行处理,这种方式叫异步非阻塞。
想一想,NIO、AIO是响应式编程吗?
此外,还有消息队列等其他方式也可以实现异步任务。
未完待续。。。