JAVA

一篇文章,搞明白异步和多线程的区别

前言


在本文中,我们通过一些通俗易懂的方式来解释异步编程和多线程编程,然后再介绍一下它们之间的区别。

在编程中,有时候我们需要处理一些费时的操作,比如网络请求、文件读写、数据库操作等等,这些操作会阻塞线程,等待结果返回。为了避免阻塞线程、提高程序的并发处理能力,我们常常采用异步编程。

什么是异步编程


首先我们先来看看一个同步的用户注册例子,流程如下:

在同步操作中,我们执行到插入数据库的时候,我们必须等待这个方法彻底执行完才能执行“发送短信”这个操作,如果插入数据库这个动作执行时间较长,发送短信需要等待,这就是典型的同步场景。

于是聪明的人们开始思考,如果两者关联性不强,能不能将一些非核心业务从主流程中剥离出来,于是有了异步编程雏形,改进后的流程如下:

这就是异步编程,它是程序并发运行的一种手段,它允许多个事件同时发生,当程序调用需要长时间运行的方法时,它不会阻塞当前的执行流程,程序可以继续运行。

异步编程是通过将耗时的任务分配给其他线程或者线程池来实现的,可以提高程序的并发处理能力,让程序具有更好的性能和响应速度。

和同步的区别

首先来看一下异步模型。在异步模型中,允许同一时间发生(处理)多个事件。程序调用一个耗时较长的功能(方法)时,它并不会阻塞程序的执行流程,程序会继续往下执行。当功能执行完毕时,程序能够获得执行完毕的消息或能够访问到执行的结果(如果有返回值或需要返回值时)。

下面通过一个示例来看一下同步和异步的区别。示例中程序通过网络获取两个文件,并对两个文件进行合并处理:

上述示例,在异步系统当中的解决方案是开启一个额外的线程进行处理。第一个线程获取第一个文件,第二个线程获取第二个文件,第二个线程并不需要等待第一个线程执行完毕再执行。当两个线程都获得到对应的结果之后,再重新同步处理合并结果的操作。

再来看另外一个场景。单线程方法读取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) {
    // 处理异步操作的异常
    // ...
}

以上就是异步编程的八种实现方式,它们各有优缺点,我们需要根据具体的需求来选择合适的实现方式。

小结


在本文中,我们讲解了异步编程和多线程编程的定义,然后是它们之间的区别。而本文中的所有术语和概念均与具体技术实现无关。后面我们会继续讲解多线程与异步相关的其他知识点,比如异步调用与回调等。