JUC

256 阅读5分钟

JUC基础

JUC概述

juc是并发编程的工具类

进程与线程

进程是系统进行资源分配和调度的基本单元,是系统中正在运行的一个应用程序。而线程是操作系统进行调度的最小单元,他被包含在进程中,是进程中的实际运作单元,是进程中独立运行的单元执行流。

线程的状态

  1. NEW(新建)
  2. RUNNABLE(准备就绪)
  3. BLOCKED(阻塞)
  4. WAITING(不见不散)
  5. TIMED_WAITING(过时不候)
  6. TERMINATED(终结)

wait()和sleep()的区别

  1. sleep 是 Thread 的静态方法,wait 是 Object 的方法,任何对象实例都能调用。
  2. sleep 不会释放锁,它也不需要占用锁。wait 会释放锁,但调用它的前提是当前线程占有锁(即代码要在 synchronized 中)
  3. 它们都可以被 interrupted 方法中断。

并发与并行

串行

一次取一个任务,执行完再取下一个

并行

一次取多个任务,交替执行,每个任务走走停停(同一个时刻,多个线程访问一个资源,多个对一点。如抢票和秒杀)

并发

多个任务同时进行(如泡面中的煲水和拆料包)

管程

保证同一个时刻只有一个进程在管程内执行,这由编译器实现,但是不能保证进程以设计的顺序执行。在一个进程获取到管程后,只有该进程释放,其他进程才能拿到管程

用户线程和守护线程

  1. 用户线程是平时用到的普通线程(如普通线程),只要用户线程未结束,jvm就不会结束。
  2. 守护线程是运行在后台的线程(如垃圾回收),即使守护线程未结束,用户线程结束,jvm也会结束。

Lock接口

Synchronized

  1. 可修饰代码块,作用于调用这个代码块的对象。
  2. 可修饰方法,作用于调用这个方法的对象(当子类方法调用到到父类的同步方法时,该子类方法也相当于同步的)
  3. 可修饰静态方法,作用于这个类的所有对象。
  4. 可修饰一个类,作用于这个类的所有对象。

什么是Lock

Lock锁比同步代码块和方法提供了更广泛的锁操作。

Lock与Synchronized的区别

  1. synchronized是java内置的,是java关键字。而Lock是一个类,也可以实现同步访问
  2. Lock实现同步要手动上锁和释放锁,而Synchronized是自动上锁和释放锁,Lock若不释放锁则有可能会造成死锁
  3. Lock 可以提高多个线程进行读操作的效率。 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源 非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于 synchronized。

Lock接口

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

lock()方法

lock()方法是用来获取锁的,通常用try catch finally修饰,并把unlock()方法放在finally快中执行为了保证锁的释放,避免死锁

Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{lock.unlock(); //释放锁
}

newCondition()方法

关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实现等待/通知模式, Lock 锁的 newContition()方法返回 Condition 对象,Condition 类也可以实现等待/通知模式。

用 notify()通知时,JVM 会随机唤醒某个等待的线程, 使用 Condition 类可以进行选择性通知,Condition 比较常用的两个方法:

  • await()会使当前线程等待,同时会释放锁,当其他线程调用 signal()时,线程会重新获得锁并继续执行。
  • signal()用于唤醒一个等待的线程。 注意:在调用 Condition 的 await()/signal()方法前,也需要线程持有相关 的 Lock 锁,调用 await()后线程会释放这个锁,在 singal()调用后会从当前 Condition 对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦 获得锁成功就继续执行。

ReentrantLock

ReentrantLock,意思是“可重入锁”,关于可重入锁的概念将在后面讲述。

ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更 多的方法。

线程间通信

线程间通信的模型有两种:共享内存和消息传递

image.png

线程间定制化通信

要添加标志位

案例如下: 问题: A 线程打印 5 次 A,B 线程打印 10 次 B,C 线程打印 15 次 C,按照此顺序循环 10 轮

class DemoClass{
 //通信对象:0--打印 A 1---打印 B 2----打印 C
 private int number = 0;
 //声明锁
 private Lock lock = new ReentrantLock();
 //声明钥匙 A
 private Condition conditionA = lock.newCondition();
 //声明钥匙 B
 private Condition conditionB = lock.newCondition();
 //声明钥匙 C
 private Condition conditionC = lock.newCondition();
 /**
 * A 打印 5 次
 */
 public void printA(int j){
 try {
 lock.lock();
 while (number != 0){
 conditionA.await();
 }
 System.out.println(Thread.currentThread().getName() + "输出 A,第" + j + "
轮开始");
 //输出 5 次 A
 for (int i = 0; i < 5; i++) {
 System.out.println("A");
 }
 //开始打印 B
 number = 1;
 //唤醒 B
 conditionB.signal();
 }catch (Exception e){
 e.printStackTrace();
 }finally {
 lock.unlock();
 }
 } /**
 * B 打印 10 次
 */
 public void printB(int j){
 try {
 lock.lock();
 while (number != 1){
 conditionB.await();
 }
 System.out.println(Thread.currentThread().getName() + "输出 B,第" + j + "
轮开始");
 //输出 10 次 B
 for (int i = 0; i < 10; i++) {
 System.out.println("B");
 }
 //开始打印 C
 number = 2;
 //唤醒 C
 conditionC.signal();
 }catch (Exception e){
 e.printStackTrace();
 }finally {
 lock.unlock();
 }
 } /**
 * C 打印 15 次
 */
 public void printC(int j){
 try {
 lock.lock();
 while (number != 2){
 conditionC.await();
 }
 System.out.println(Thread.currentThread().getName() + "输出 C,第" + j + "
轮开始");
 //输出 15 次 C
 for (int i = 0; i < 15; i++) {
 System.out.println("C");
 }
 System.out.println("-----------------------------------------");
 //开始打印 A
 number = 0;
 //唤醒 A
 conditionA.signal();
 }catch (Exception e){
 e.printStackTrace();
 }finally {
 lock.unlock();
 } }
}

测试类

/**
* volatile 关键字实现线程交替加减
*/
public class TestVolatile {
 /**
 * 交替加减
 * @param args
 */
 public static void main(String[] args){
 DemoClass demoClass = new DemoClass();
 new Thread(() ->{
 for (int i = 1; i <= 10; i++) {
 demoClass.printA(i);
 }
 }, "A 线程").start();
 new Thread(() ->{
 for (int i = 1; i <= 10; i++) { demoClass.printB(i);
 }
 }, "B 线程").start();
 new Thread(() ->{
 for (int i = 1; i <= 10; i++) {
 demoClass.printC(i);
 }
 }, "C 线程").start();
 }
}

注意

image.png

集合线程安全

List线程不安全解决?

  • Vector

    Vector 是矢量队列,它是 JDK1.0 版本添加的类。继承于 AbstractList,实现 了 List, RandomAccess, Cloneable 这些接口。 Vector 继承了 AbstractList, 实现了 List;所以,它是一个队列,支持相关的添加、删除、修改、遍历等功 能。 Vector 实现了 RandmoAccess 接口,即提供了随机访问功能。 RandmoAccess 是 java 中用来被 List 实现,为 List 提供快速访问功能的。在 Vector 中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访 问。 Vector 实现了 Cloneable 接口,即实现 clone()函数。它能被克隆。 ==和 ArrayList 不同,Vector 中的操作是线程安全的。==

  • Collections

    Collections 提供了方法 synchronizedList 保证 list 是同步线程安全的

  • CopyOnWriteArrayList(重点)

    首先我们对 CopyOnWriteArrayList 进行学习,其特点如下: 它相当于线程安全的 ArrayList。和 ArrayList 一样,它是个可变数组;但是和 ArrayList 不同的时,它具有以下特性:

  1. 它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。

  2. 它是线程安全的。

  3. 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。

  4. 迭代器支持 hasNext(), next()等不可变操作,但不支持可变 remove()等操作。

  5. 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代 器时,迭代器依赖于不变的数组快照。

  6. 独占锁效率低:采用读写分离思想解决

  7. 写线程获取到锁,其他写线程阻塞

  8. 复制思想: 当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容 器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素 之后,再将原容器的引用指向新的容器。 这时候会抛出来一个新的问题,也就是数据不一致的问题。如果写线程还没来 得及写会内存,其他的线程就会读到了脏数据。

==这就是 CopyOnWriteArrayList 的思想和原理。就是拷贝一份。==

原因分析(重点): ==动态数组与线程安全==

下面从“动态数组”和“线程安全”两个方面进一步对CopyOnWriteArrayList 的原理进行说明。

  • “动态数组”机制
    • 它内部有个“volatile 数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile 数组”, 这就是它叫做 CopyOnWriteArrayList 的原因
    • 由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList 效率很低;但是单单只是进行遍历查找的话,效率比较高。
  • “线程安全”机制
    • 通过 volatile 和互斥锁来实现的。
    • 通过“volatile 数组”来保存数据的。一个线程读取 volatile 数组时,总能看到其它线程对该 volatile 变量最后的写入;就这样,通过 volatile 提供了“读取到的数据总是最新的”这个机制的保证。
    • 通过互斥锁来保护数据。在“添加/修改/删除”数据时,会先“获取互斥锁”,再修改完毕之后,先将数据更新到“volatile 数组”中,然后再“释放互斥锁”,就达到了保护数据的目的。

Callable&Future 接口

为什么要用Callable接口创建线程

用于返回结果

引入FutureTask

java 库具有具体的 FutureTask 类型,该类型实现 Runnable 和 Future,并方 便地将两种功能组合在一起。 可以通过为其构造函数提供 Callable 来创建 FutureTask。然后,将 FutureTask 对象提供给 Thread 的构造函数以创建 Thread 对象。因此,间接地使用 Callable 创建线程。

  • 当主线程将来需要时,就可以通过 Future 对象获得后台作业的计算结果或者执行状态
  • 一般 FutureTask 多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
  • 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法
  • 一旦计算完成,就不能再重新开始或取消计算
  • get 方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常
  • get 只计算一次,因此 get 方法放到最后

JUC三大辅助类

JUC 中提供了三种常用的辅助类,通过这些辅助类可以很好的解决线程数量过 多时 Lock 锁的频繁操作。这三种辅助类为:

  • CountDownLatch: 减少计数
  • CyclicBarrier: 循环栅栏
  • Semaphore: 信号灯

减少计数CountDownLatch

CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行 减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法 之后的语句。它是一个同步工具类,允许一个或多个线程一直等待,直到其他线程运行完成后再执行。

  • CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这些线程会阻塞
  • 其它线程调用 countDown 方法会将计数器减 1(调用 countDown 方法的线程不会阻塞)
  • 当计数器的值变为 0 时,因 await 方法阻塞的线程会被唤醒,继续执行

image.png 应用举例:场景: 6 个同学陆续离开教室后值班同学才可以关门。

循环栅栏 CyclicBarrier

CyclicBarrier 的构造方法第一个参数是目标障碍数,第二个参数是当达到目标障碍数时要执行的线程。如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后的语句。可以将 CyclicBarrier.await() 理解为加 1 操作。

应用举例:场景: 集齐 7 颗龙珠就可以召唤神龙

CountDownLatch与CyclicBarrier

  • CyclicBarrier 方法多,可以用reset()方法来重置CyclicBarrier,让栅栏可以反复用。而CountDownLatch如果count变为0了,那么只能保持在这个0的最终状态,不能重新再用。
  • CyclicBarrier 是让一组线程等待某个事件发生,如果发生了,这组线程可以继续执行;CountDownLatch是一个线程或多个线程等待一组线程执行完毕。不同的点就在于当count变为0之后,CyclicBarrier是让这组线程不再阻塞,而继续执行;而CountDownLatch是让等待的线程不阻塞,继续执行。

信号灯 Semaphore

Semaphore 的构造方法中传入的第一个参数是最大信号量(可以看成最大线 程池),每个信号量初始化为一个最多只能分发一个许可证。使用 acquire 方 法获得许可证,release 方法释放许可.

应用场景:多个线程抢占有限资源。如(抢车位, 6 部汽车 3 个停车位)

读写锁

现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那 么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以 应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源, 就不应该允许其他线程对该资源进行读和写的操作了。 针对这种场景,JAVA 的并发包提供了读写锁 ReentrantReadWriteLock, 它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称 为排他锁

线程进入读锁的前提条件:

  • 没有其他线程的写锁
  • 没有其他线程的写有写请求,但调用线程和持有锁的线程是同一个(可重入锁)

线程进入写锁的前提条件

  • 没有其他线程的写锁和读锁。

读写锁有以下三个重要的特性

  1. 公平选择性(但是非公平锁的吞吐量更高,因为减少了切换上下文的资源的消耗)
  2. 可重入性
  3. 写锁可降级,读锁不可升级。(写锁降级流程:获取写锁,再获取读锁,先释放写锁)

阻塞队列

Concurrent 包中,BlockingQueue 很好的解决了多线程中,如何高效安全 “传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建 高质量的多线程程序带来极大的便利。本文详细介绍了 BlockingQueue 家庭 中的所有成员,包括他们各自的功能以及常见使用场景。

image.png

为什么需要BlockingQueue

减少了程序员对多线程的工作,不要考虑什么时候阻塞线程和唤醒线程,使程序员能更加专注于逻辑代码。这都是有阻塞队列给包办了。

在 concurrent 包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。

BlockingQueue 核心方法

image.png

常见的BlockingQueue

  • ArrayBlockingQueue(常用):由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue(常用):由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
  • DelayQueue:使用优先级队列实现的延迟无界阻塞队列。往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列。不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。
  • SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
  • LinkedTransferQueue:由链表组成的无界阻塞队列。
  • LinkedBlockingDequ:由链表组成的双向阻塞队列

ThreadPool 线程池

线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。

主要特点

  • 降低了资源的消耗
  • 提高了响应速度
  • 提高了线程的可管理性

创建线程池的参数

  • corePoolSize 线程池的核心线程数
  • maximumPoolSize 能容纳的最大线程数
  • keepAliveTime 空闲线程存活时间
  • unit 存活的时间单位
  • workQueue 存放提交但未执行任务的队列
  • threadFactory 创建线程的工厂类
  • handler 等待队列满后的拒绝策略 线程池中,有三个重要的参数,决定影响了拒绝策略:corePoolSize - 核心线 程数,也即最小的线程数。workQueue - 阻塞队列 。 maximumPoolSize - 最大线程数 当提交任务数大于 corePoolSize 的时候,会优先将任务放到 workQueue 阻 塞队列中。当阻塞队列饱和后,会扩充线程池中线程数,直到达到maximumPoolSize 最大线程数配置。此时,再多余的任务,则会触发线程池 的拒绝策略了。 总结起来,也就是一句话,当提交的任务数大于(workQueue.size() + maximumPoolSize ),就会触发线程池的拒绝策略。

拒绝策略(重点)

  • CallerRunsPolicy: 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度快,可能导致程序阻塞,性能效率上必然的损失较大
  • AbortPolicy: 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
  • DiscardPolicy: 直接丢弃,其他啥都没有
  • DiscardOldestPolicy: 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入

线程池的种类与创建

创建

使用Executors工具类创建

种类

  • newCachedThreadPool(常用)

    • 作用:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
    • 特点:线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE)。线程池中的线程可进行缓存重复利用和回收(回收默认时间为 1 分钟)。当线程池中,没有可用线程,会重新创建一个线程
    • 场景:适用于创建一个可无限扩大的线程池,服务器负载压力较轻,执行时间较短,任务多的场景
  • newFixedThreadPool(常用)

    • 作用:创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
    • 特征: 线程池中的线程处于一定的量,可以很好的控制线程的并发量。线程可以重复被使用,在显示关闭之前,都将一直存在。超出一定量的线程被提交时候需在队列中等待
    • 场景: 适用于可以预测线程数量的业务中,或者服务器负载较重,对线程数有严格限制的场景
  • newSingleThreadExecutor(常用)

    • 作用:创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的newFixedThreadPool 不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。
    • 特征: 线程池中最多执行 1 个线程,之后提交的线程活动将会排在队列中以此执行
    • 场景: 适用于需要保证顺序执行各个任务,并且在任意时间点,不会同时有多个线程的场景
  • newScheduleThreadPool(了解)

    • 作用: 线程池支持定时以及周期性执行任务,创建一个 corePoolSize 为传入参数,最大线程数为整形的最大数的线程池
    • 特征:(1)线程池中具有指定数量的线程,即便是空线程也将保留 (2)可定时或者延迟执行线程活动
    • 场景: 适用于需要多个后台线程执行周期任务的场景
  • newWorkStealingPool

    • 概述:jdk1.8 提供的线程池,底层使用的是 ForkJoinPool 实现,创建一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用 cpu 核数的线程来并行执行任务
    • 场景: 适用于大耗时,可并行执行的场景

为什么不允许使用 Executors.的方式手动创建线程池,如下图

image.png

Fork/Join

Fork/Join 框架简介

Fork/Join 它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子 任务结果合并成最后的计算结果,并进行输出。Fork/Join 框架要完成两件事 情:

  • Fork:将任务拆分成小任务
  • Join:将小任务的计算结果合并

怎么用?

  • ForkJoinTask:我们要使用 Fork/Join 框架,首先需要创建一个 ForkJoin 任务。该类提供了在任务中执行 fork 和 join 的机制。通常情况下我们不需要直接集成 ForkJoinTask 类,只需要继承它的子类,Fork/Join 框架提供了两个子类:
    • RecursiveAction:用于没有返回结果的任务
    • RecursiveTask:用于有返回结果的任务
  • ForkJoinPool:ForkJoinTask 需要通过 ForkJoinPool 来执行
  • RecursiveTask: 继承后可以实现递归(自己调自己)调用的任务

Fork/Join 框架的实现原理

ForkJoinPool 由 ForkJoinTask 数组和 ForkJoinWorkerThread 数组组成, ForkJoinTask 数组负责将存放以及将程序提交给 ForkJoinPool,而 ForkJoinWorkerThread 负责执行这些任务。

案例

/**
* 递归累加
*/
public class TaskExample extends RecursiveTask<Long> {
    private int start;
    private int end;
    private long sum;
    /**
    * 构造函数
    * @param start
    * @param end
    */
    public TaskExample(int start, int end){
        this.start = start;
        this.end = end;
    }
    /**
    * The main computation performed by this task.
    *
    * @return the result of the computation
    */@Override
    protected Long compute() {
        System.out.println("任务" + start + "=========" + end + "累加开始");
        //大于 100 个数相加切分,小于直接加
        if(end - start <= 100){
            for (int i = start; i <= end; i++) {
                //累加
                sum += i;
            }
        }else {
        //切分为 2 块
            int middle = start + 100;
            //递归调用,切分为 2 个小任务
            TaskExample taskExample1 = new TaskExample(start, middle);
            TaskExample taskExample2 = new TaskExample(middle + 1, end);
            //执行:异步
            taskExample1.fork();
            taskExample2.fork();
            //同步阻塞获取执行结果
            sum = taskExample1.join() + taskExample2.join();
        }
        //加完返回
        return sum;
    }
}

/**
* 分支合并案例
*/public class ForkJoinPoolDemo {
 /**
 * 生成一个计算任务,计算 1+2+3.........+1000
 * @param args
 */
 public static void main(String[] args) {
 //定义任务
 TaskExample taskExample = new TaskExample(1, 1000);
 //定义执行对象
 ForkJoinPool forkJoinPool = new ForkJoinPool();
 //加入任务执行
 ForkJoinTask<Long> result = forkJoinPool.submit(taskExample);
 //输出结果
 try {
 System.out.println(result.get());
 }catch (Exception e){
 e.printStackTrace();
 }finally {
 forkJoinPool.shutdown();
 }
 }
}

CompletableFuture(实现异步回调)

CompletableFuture 在 Java 里面被用于异步编程,异步通常意味着非阻塞, 可以使得我们的任务单独运行在与主线程分离的其他线程中,并且通过回调可 以在主线程中得到异步任务的执行状态,是否完成,和是否异常等信息。

CompletableFuture 实现了 Future, CompletionStage 接口,实现了 Future 接口就可以兼容现在有线程池框架,而 CompletionStage 接口才是异步编程 的接口抽象,里面定义多种异步方法,通过这两者集合,从而打造出了强大的 CompletableFuture 类。

但是业务中通常使用mq实现异步回调。