前言
在本文中,我们通过一些通俗易懂的方式来解释异步编程和多线程编程,然后再介绍一下它们之间的区别。
在编程中,有时候我们需要处理一些费时的操作,比如网络请求、文件读写、数据库操作等等,这些操作会阻塞线程,等待结果返回。为了避免阻塞线程、提高程序的并发处理能力,我们常常采用异步编程。
什么是异步编程
首先我们先来看看一个同步的用户注册例子,流程如下:
在同步操作中,我们执行到插入数据库的时候,我们必须等待这个方法彻底执行完才能执行“发送短信”这个操作,如果插入数据库这个动作执行时间较长,发送短信需要等待,这就是典型的同步场景。
于是聪明的人们开始思考,如果两者关联性不强,能不能将一些非核心业务从主流程中剥离出来,于是有了异步编程雏形,改进后的流程如下:
这就是异步编程,它是程序并发运行的一种手段,它允许多个事件同时发生,当程序调用需要长时间运行的方法时,它不会阻塞当前的执行流程,程序可以继续运行。
异步编程是通过将耗时的任务分配给其他线程或者线程池来实现的,可以提高程序的并发处理能力,让程序具有更好的性能和响应速度。
和同步的区别
首先来看一下异步模型。在异步模型中,允许同一时间发生(处理)多个事件。程序调用一个耗时较长的功能(方法)时,它并不会阻塞程序的执行流程,程序会继续往下执行。当功能执行完毕时,程序能够获得执行完毕的消息或能够访问到执行的结果(如果有返回值或需要返回值时)。
下面通过一个示例来看一下同步和异步的区别。示例中程序通过网络获取两个文件,并对两个文件进行合并处理:
上述示例,在异步系统当中的解决方案是开启一个额外的线程进行处理。第一个线程获取第一个文件,第二个线程获取第二个文件,第二个线程并不需要等待第一个线程执行完毕再执行。当两个线程都获得到对应的结果之后,再重新同步处理合并结果的操作。
再来看另外一个场景。单线程方法读取OS(操作系统)当中的文件并需要进行数学运算。而在异步系统中,程序发起读取OS中文件的请求,由于读取操作比较耗时,在等待读取文件时,程序会将控制器返回给CPU进行数学运算。
在异步编程中,通常会针对比较耗时的功能提供一个函数,函数的参数中包含一个额外的参数,用于回调。而这个函数往往称作回调函数。当比较耗时的功能执行完毕时,通过回调函数将结果返回。关于回调函数相关的知识可参考文章《两个经典例子让你彻底理解java回调机制》。
异步的本质是将任务提交给其他线程或者线程池来处理,等待结果时,当前线程不会被阻塞,可以继续处理其他任务。
什么是多线程编程
多线程是指同时并发或并行执行多个指令(线程)。
在单核处理器上,多线程往往会给人程序是在并行执行的错觉。实际上,处理器是通过调度算法在多线程之间进行切换和调度。或者根据外部输入(中断)和线程的优先级的组合来进行线程的切换。
在多核处理器上,线程才是真正的并行运行。多个处理器同时执行多个线程,以达到更加高效的处理。
一个简单的示例就是:开启两个浏览器窗口同时下载两个文件。每个窗口都使用一个新的线程去下载文件,它们之间并不需要谁等待谁完成,而是并行进行下载。
下图展示了并发执行多线程应用程序的流程:
异步与多线程的区别
通过上面的介绍,我们可以看出多线程都是关于功能的并发执行。而异步编程是关于函数之间的非阻塞执行,我们可以将异步应用于单线程或多线程当中。
因此,多线程只是异步编程的一种实现形式。
比如,你和你的朋友决定一起做一顿午餐。“异步”就是你对朋友说:“你去商店买意大利面,回来的时候告诉我一声,然后一起做午餐。在你买意大利面的同时,我去准备番茄酱和饮料。”
而“线程”是:“你烧水,我加热番茄酱。当水烧开了,告诉我,我把意大利放进去。当番茄酱热了,你可以把奶酪添加进去。当两者都完成了,就可以坐下来一起吃晚餐。”在线程的示例中,我们可以看到“When,Do”的事件顺序,而这些顺序代表着每个人(线程)的指令集集合的顺序。
上述示例可以看出,多线程是与具体的执行者相关的,而异步是与任务相关的。
多线程是程序设计的逻辑层概念,它是进程中并发运行的一段代码,可以实现线程间的切换执行。
异步和同步是相对的,异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作。
多线程就是实现异步的一个方式。异步是让调用方法的主线程不需要同步等待另一线程的完成,从而可以让主线程干其它的事情。
所以本质上,异步和多线程并不是一个同等关系,异步是最终目的,多线程只是实现异步的一种手段。
如何选择
面对多线程和异步,我们该如何选择呢?其实,通常情况下选择的依据是主要取决于性能。
那么,同步/异步与单线程/多线程之间的所有组合,哪种模型表现更好?
简而言之,对于具有大量I/O操作和不同计算的大规模应用程序,使用异步多线程有利于充分利用计算资源,并且能够照顾到非阻塞函数。这也是所有操作系统所采用的线程模型。
编写异步操作的复杂程度较高,程序主要使用回调方式进行处理,与正常的思维方式有些出入,而且难以调试。而多线程的使用(滥用)会给系统带来上下文切换的额外负担,并且线程间的共享变量可能造成死锁。
因此在实现这两种模式时,往往需要处理资源竞争、死锁、共享资源和回调事件等问题。
JAVA 实现异步的8种方式
- 线程异步
线程异步是一种最为基础的异步实现方式,它通过创建一个新的线程来执行耗时操作,从而避免阻塞主线程。
在 Java 语言中最简单使用异步编程的方式就是创建一个 线程来实现,如果你使用的 JDK 版本是 8 以上的话,可以使用 Lambda 表达式 会更加简洁。
当然如果每次都创建一个 Thread线程,频繁的创建、销毁,浪费系统资源,我们可以采用线程池
线程异步的示例代码如下:
public void doAsyncTask() { Thread thread = new Thread(() -> { // 耗时操作 doHeavyTask(); }); thread.start(); } private void doHeavyTask() { // 耗时操作 // ... }
- Future异步
Future异步是通过使用Java的Future接口来实现的,它提供了异步编程的基础能力。
Future 类通过 get() 方法阻塞等待获取异步执行的运行结果,性能比较差。
Future异步的示例代码如下:
public void doAsyncTask() { ExecutorService executorService = Executors.newCachedThreadPool(); Future<Result> future = executorService.submit(() -> { // 耗时操作 return doHeavyTask(); }); // 在这里处理异步操作的结果 try { Result result = future.get(); handleResult(result); } catch (Exception e) { handleError(e); } } private Result doHeavyTask() { // 耗时操作 // ... return new Result(); } private void handleResult(Result result) { // 处理异步操作的结果 // ... } private void handleError(Exception e) { // 处理异步操作的异常 // ... }
- CompletableFuture实现异步
CompletableFuture是Java 8中引入的一个异步编程工具类,它提供了更加方便的异步编程方式。
JDK1.8 中,Java 提供了 CompletableFuture 类,它是基于异步函数式编程。相对阻塞式等待返回结果,CompletableFuture 可以通过回调的方式来处理计算结果,实现了异步非阻塞,性能更优。
CompletableFuture实现异步的示例代码如下:
public void doAsyncTask() { CompletableFuture<Result> future = CompletableFuture.supplyAsync(() -> { // 耗时操作 return doHeavyTask(); }); // 处理异步操作的结果 future.thenAccept(this::handleResult) .exceptionally(this::handleError); } private Result doHeavyTask() { // 耗时操作 // ... return new Result(); } private void handleResult(Result result) { // 处理异步操作的结果 // ... } private void handleError(Throwable throwable) { // 处理异步操作的异常 // ... }
- Spring的@Async异步
Spring框架提供了@Async注解来实现异步编程,它可以将一个方法标记为异步执行,并使用ThreadPoolTaskExecutor来执行耗时操作。
Spring的@Async异步的示例代码如下:
@Service public class MyService { @Async public void doAsyncTask() { // 耗时操作 doHeavyTask(); } private void doHeavyTask() { // 耗时操作 // ... } }
- Spring ApplicationEvent事件实现异步
Spring还提供了ApplicationEvent异步事件,可以在事件监听器中处理耗时操作,从而实现异步编程。
Spring ApplicationEvent事件实现异步的示例代码如下:
@Component public class MyEventListener { @EventListener @Async public void handleEvent(MyEvent event) { // 耗时操作 doHeavyTask(); } private void doHeavyTask() { // 耗时操作 // ... } }
- 消息队列
消息队列是一种基于异步消息传递的异步编程方式,它将消息放入队列中,异步处理这些消息。
常见的消息队列有ActiveMQ、RabbitMQ、RocketMQ等等。
消息队列的示例代码如下:
public void doAsyncTask() { // 发送异步消息 sendAsyncMessage(); // 处理其他任务 // ... } private void sendAsyncMessage() { // 将消息发送到消息队列中 // ... }
- ThreadUtil异步工具类
ThreadUtil是一个Java工具类,提供了很多方便的异步编程方法,例如:线程池、定时器、异步调度等等。
ThreadUtil异步工具类的示例代码如下:
public void doAsyncTask() { // 使用线程池执行异步任务 ThreadUtil.execAsync(() -> { // 耗时操作 doHeavyTask(); }); // 处理其他任务 // ... } private void doHeavyTask() { // 耗时操作 // ... }
- Guava异步
Guava是一个Google开发的Java工具类库,提供了很多常用的工具类和方法,其中包括异步编程的实现方式。
Guava异步的示例代码如下:
public void doAsyncTask() { ListenableFuture<Result> future = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()) .submit(() -> { // 耗时操作 return doHeavyTask(); }); Futures.addCallback(future, new FutureCallback<Result>() { @Override public void onSuccess(@Nullable Result result) { handleResult(result); } @Override public void onFailure(Throwable throwable) { handleError(throwable); } }); } private Result doHeavyTask() { // 耗时操作 // ... return new Result(); } private void handleResult(Result result) { // 处理异步操作的结果 // ... } private void handleError(Throwable throwable) { // 处理异步操作的异常 // ... }
以上就是异步编程的八种实现方式,它们各有优缺点,我们需要根据具体的需求来选择合适的实现方式。
小结
在本文中,我们讲解了异步编程和多线程编程的定义,然后是它们之间的区别。而本文中的所有术语和概念均与具体技术实现无关。后面我们会继续讲解多线程与异步相关的其他知识点,比如异步调用与回调等。