前言
异步编程是可以让程序并行运行的一种手段,其可以让程序中的一个工作单元与主应用程序线程分开独立运行,并且等工作单元运行结束后通知主应用程序线程它的运行结果或者失败原因。使用它有许多好处,例如改进的应用程序性能和减少用户等待时间等。
比如线程 A 要做从数据库 I 和数据库 II 查询一条记录,并且把两者结果拼接起来作为前端展示使用,如线程 A 是同步调用两次查询,则整个过程耗时时间为访问数据库 I 的耗时加上访问数据库 II 的耗时,如下图:
如果为异步调用则可以在线程 A 内开启一个异步运行单元来从数据库 I 获取数据,然后线程 A 本身来从数据库 II 获取数据,并且等两者结果都返回后,在拼接两者结果,这时候整个过程耗时为 max(线程 A 从数据库 II 获取数据耗时,异步运行单元从数据库 I 获取数据耗时),如下图:
可见整个过程耗时有显著缩短,对于用户来说页面响应时间会更短,对用户体验会更好,其中异步单元一般是线程池中的线程。
其实上面对异步编程的定义有点问题,其定义异步编程是可以让程序并行运行的一种手段,这里并行应该改为并发,因为并发与并行是有本质区别的,并发是指同一个时间段内多个任务同时都在执行,并且都没有执行结束,而并行是说在单位时间内多个任务同时在执行,并发任务强调在一个时间段内同时执行,而一个时间段有多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行。
在单个 cpu 的时代多个任务同时运行都是并发,这是因为 cpu 同时只能执行一个任务,单个 cpu 时代多任务是共享一个 cpu 的,当一个任务占用 cpu 运行时候,其它任务就会被挂起,当占用 cpu 的任务时间片用完后,会把 cpu 让给其它任务来使用,所以在单 cpu 时代多线程编程是没有意义的,并且线程间频繁的上下文切换还会带来开销。
如下图单个 cpu 上运行两个线程,可知线程 A 和 B 是轮流使用 cpu 进行任务处理的,也就是同时 CPU 只在执行一个线程上面的任务,当前线程 A 的时间片用完后会进行线程上下文切换,也就是保存当前线程的执行线程,然后切换线程 B 占用 cpu 运行任务。
如下图双 cpu 时候,线程 A 和线程各自在自己的 CPU 上执行任务,实现了真正的并行运行。
而在多线程编程实践中线程的个数往往多于 CPU 的个数,所以平时都是称多线程并发编程而不是多线程并行编程。
使用 Thread&Runnable 实现异步编程
在 Java 中最简单的是创建一个 Thread 来实现异步编程,比如在同步编程下我们在一个线程中要做两件事情代码大概是如下面所示:
public class SyncExample {
public static void doSomethingA() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("--- doSomethingA---");
}
public static void doSomethingB() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("--- doSomethingB---");
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
// 1.执行任务 A
doSomethingA();
// 2.执行任务 B
doSomethingB();
System.out.println(System.currentTimeMillis() - start);
}
}
如上代码 main 线程内首先执行了 doSomethingA 方法,然后执行了 doSomethingB 方法,那么整个过程耗时为 4s 时间,如果开启一个线程来异步执行任务 doSomethingA,main 函数所在线程执行 doSomethingB 则可以大大缩短整个任务处理耗时,上面 main 函数代码可以修改为如下:
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
// 1.开启异步单元执行任务 A
Thread thread = new Thread(() -> {
try {
doSomethingA();
} catch (Exception e) {
e.printStackTrace();
}
}, "threadA");
thread.start();
// 2.执行任务 B
doSomethingB();
// 3.同步等待线程 A 运行结束
thread.join();
System.out.println(System.currentTimeMillis() - start);
}
如上代码 1 我们在 main 函数所在线程内开启了一个线程 A 用来异步执行 doSomethingA 任务,这时候线程 A 与 main 线程并发运行,也就是任务 doSomethingA 与任务 doSomethingB 并发运行,代码 3 则等 main 线程运行完 doSomethingB 任务后同步等待线程 A 运行完毕,运行上面代码,打印结果可知这时候整个过程耗时 2s 左右,可知使用异步编程可以大大缩短任务运行时间。但是上述代码存在两个问题:
每当执行异步任务时候直接创建了一个 Thread 来执行异步任务,这在生产实践中是不建议使用的,这是因为线程创建与销毁是有开销的,并且没有限制线程的个数,如果使用不当可能会把系统线程用尽,从而造成错误。在生产环境中一般是创建一个线程池,然后使用线程池中的线程来执行异步任务,线程池中的线程是可以被复用的,这可以大大减少线程创建与销毁开销。
上面使用 Thread 执行的异步任务并没有返回值,如果我们想异步执行一个任务,并且需要在任务执行完毕后获取任务执行结果,则上面这个方式是满足不了的,这时候就需要 JDK 中的 Future 了。
FutureTask 实现异步编程
FutureTask 代表了一个可被取消的异步计算任务,该类提供了 Future 接口的实现,比如提供了开启和取消任务、查询任务是否完成、获取计算结果的接口。