面试题整理:Java多线程(一)volatile、synchronized、ReentrantLock、Atomic、ThreadLocal、AQS、Future

发布于:2025-02-10 ⋅ 阅读:(52) ⋅ 点赞:(0)

本文主要参考的是javaguide,由于java的多线程、jvm我都没有系统学过,所以基本每个点都很生疏,学习时顺便记录了一下避免自己跑神睡觉,八股先跳过了java基础,这次先学习了一些并发编程的关键字和类。

文章目录

Java多线程(一)

volatile关键字

1. volatile关键字的作用和特点?

作用:

  • volatile关键字用于在并发编程中保证变量可见性,被volatile修饰的变量是共享的、不稳定的,每次都在主存中读取。
  • volatile关键字可以防止JVM指令重排序。
  • volatile 关键字不能保证对变量的操作是原子性的。
    • 原子性还得看Synchronized、Lock、原子变量AtomicInteger
  • 双重检验锁方式实现单例模式(线程安全)由于 JVM 具有指令重排的特性,uniqueInstance = new Singleton()执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }
    //只有getInstance是public的
    public  static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                    //1. 为 uniqueInstance 分配内存空间
                    //2. 初始化 uniqueInstance
                    //3. 将 uniqueInstance 指向分配的内存地址
                }
            }
        }
        return uniqueInstance;
    }
}
  • 比如对public volatile static int inc = 0;的inc操作inc++分为①从读取inc的值,inc值加1,inc值写回内存。volatile无法保证这三个操作具有原子性。

    改进办法:对操作函数使用synchronized加锁、使用AtomicInteger对象、使用ReentrantLock[riːˈentrənt lɒk]类加锁。

2. ⭐synchronized 和 volatile 有什么区别?

互补而非对立。

解决的问题:volatile主要用于解决变量在多个线程间的可见性,synchronized解决多个线程访问资源的同步性。

特性:synchronized可以保证可见性、有序性、原子性,volatile只能实现可见性和有序性。

性能:volatile是synchronized轻量级实现,比synchronized性能好。

修饰对象:volatile只能用于变量,synchronized还可修饰方法和代码块。

synchronized关键字

在 Java 中,synchronized 关键字用于实现同步机制,它可以保证在同一时刻只有一个线程可以访问被 synchronized 修饰的代码块或方法。

1. 如何使用 synchronized?

synchronized 关键字的使用方式主要有下面 3 种:

  1. 修饰实例方法->给对象实例上锁
  2. 修饰静态方法->给类上锁
  3. 修饰代码块
    1. synchronized(object) 表示进入同步代码库前要获得 给定对象的锁
    2. synchronized(类名.class) 表示进入同步代码前要获得 给定 Class 的锁

静态 synchronized 方法和非静态 synchronized 方法之间的调用互斥么?不互斥!如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

2. synchronized能否修饰构造函数?

不能。构造方法本身是线程安全的,如果在构造方法中涉及到共享资源的操作,需要采取同步措施,比如可以用synchronized代码块。

3. synchronized关键字的特性是什么?

synchronized关键字可以保证并发编程的三大特性:原子性、可见性、有序性,而volatile关键字只能保证可见性和有序性,不能保证原子性,也称为是轻量级的synchronized

原子性:一个或多个操作全部执行成功或者全部执行失败。synchronized关键字可以保证只有一个线程拿到锁,访问共享资源。

可见性:当一个线程对共享变量进行修改后,其他线程可以立刻看到。

有序性:程序的执行顺序会按照代码的先后顺序执行。

4. synchronized关键字可以实现什么类型的锁?

悲观锁:synchronized关键字实现的是悲观锁,每次访问共享资源时都会上锁。

非公平锁:synchronized关键字实现的是非公平锁,即线程获取锁的顺序并不一定是按照线程阻塞的顺序。

可重入锁:synchronized关键字实现的是可重入锁,即已经获取锁的线程可以再次获取锁。

独占锁或者排他锁:synchronized关键字实现的是独占锁,即该锁只能被一个线程所持有,其他线程均被阻塞。

5. ⭐synchronized的底层原理是什么?

synchronized关键字的底层原理主要依赖监视器锁(Monitor),通过进入和退出Monitor对象。

在Java虚拟机HotSpot中,monitor是由ObjectMonitor实现的,其源码是用C++语言编写的。ObjectMonitor数据结构中,包括锁计数器_count、等待线程数_waiters 、锁重入次数_recursions、Monitor对象_object、持有ObjectMonitor对象的线程_owner、wait状态的线程列表_WaitSet、等待锁状态的线程列表_EntryList

synchronized 修饰代码块

当用 synchronized 修饰代码块时,编译后的字节码会有 monitorenter monitorexit指令,分别对应的是获得锁和解锁。enter和exit两个指令保证了代码是否顺利都能释放锁。

synchronized 修饰方法

当用 synchronized 修饰方法时,会给方法加上ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM在进入方法时会进行锁竞争。

竞争Monitor的实现细节

  1. 当多个线程同时访问一段同步代码时,首先会进入 _EntryList 队列。
  2. (通过CAS的方法)尝试获取Monitor对象,锁的计数器为0表示可以获取到。获取到之后把_owner字段设置为当前线程,进入临界区,Monitor对象的锁计数器加1,_recursions加1 。
  3. 若持有Monitor的线程调用 wait() 方法,将释放当前持有的Monitor,_owner变量恢复为null,_count减1,_recursions减1,同时该线程进入 _WaitSet 集合中等待被唤醒。在_WaitSet 集合中的线程被唤醒后,再次放到_EntryList 队列中,重新竞争获取锁。
  4. 执行完同步代码块,释放锁,_count减1,_recursions减1,如果_recursions 减到 0,就说明线程需要释放锁了。释放Monitor并复位变量的值,以便其他线程进入获取锁。

可重入锁是根据 _recursions 来判断的,重入一次就执行 _recursions++,解锁一次就执行 _recursions--,如果 _recursions 减到 0 ,就说明需要释放锁了。

6. JDK1.6 之后的 sychronized 底层做了哪些优化?锁升级原理了解吗 ?

在 Java 6 之后, synchronized 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 synchronized 锁的效率提升了很多(JDK18 中,偏向锁已经被彻底废弃)

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

浅析synchronized锁升级的原理与实现 - 小新成长之路 - 博客园

7. 偏向锁为什么被废弃了?
  1. 性能收益不明显。偏向锁可以提升单线程对同步代码块的访问性能。受益于偏向锁的应用程序通常使用了早期的 Java 集合 API,例如 HashTableVector,这些集合类中通过 synchronized 来控制同步,这样在单线程频繁访问时,通过偏向锁会减少同步开销。随着 JDK 的发展,出现了 ConcurrentHashMap 高性能的集合类,在集合类内部进行了许多性能优化,此时偏向锁带来的性能收益就不明显了。

    偏向锁仅在单线程访问同步代码块的场景中可以获得性能收益。如果存在多线程竞争,需要撤销偏向锁 ,撤销需要等待进入到全局安全点,该状态下所有线程都是暂停的,此时去检查线程状态并进行偏向锁的撤销。

  2. JVM 内部代码维护成本太高。偏向锁将许多复杂代码引入到同步子系统,并且对其他的 HotSpot 组件也具有侵入性。这种复杂性为理解代码、系统重构带来了困难,因此, OpenJDK 官方希望禁用、废弃并删除偏向锁。

👉AQS抽象类

1. AQS是什么?有什么作用?

AQS全称AbstractQueuedSynchronizer抽象队列同步器,为同步器提供了通用的执行框架,定义了资源获取、释放的通用流程,可以理解为AQS作为同步器的基础模板,同步器是基于AQS实现的具体应用。

作用是用来构建锁和同步器(例如可重入锁ReentrantLock、信号量Semaphore和 倒计时器CountDownLatch),它封装了复杂的线程管理逻辑,使得开发者专注于具体的同步逻辑。

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
}
2. ⭐AQS的原理是什么?

AQS的核心思想是:对于共享资源,若空闲则将当前请求线程设为有效工作线程并锁定资源;若被占用,则通过CLH队列锁机制,将获取不到锁的线程加入队列,实现线程阻塞等待与唤醒时的锁分配。

CLH锁队列是对自旋锁的改进,是基于的单链表(prev)的自旋锁,每个等待的线程会自旋访问前一个线程节点的状态,前一个节点释放锁后当前节点可以获取锁。

AQS中的等待队列是CLH锁队列变体,主要变化点①自旋+阻塞。②虚拟双向队列(prev、next)。先通过自旋尝试获取锁,如果失败再进行阻塞等待,当队列前边的线程释放锁之后,需要对后边的线程进行唤醒。AQS将请求共享资源的线程封装成CLH变体队列的节点来实现锁分配,CLH变体队列中节点包含线程引用、状态waitStatus、前驱和后继节点。

AQS维护了一个volatile int state代表共享资源,和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。AQS定义两种资源共享方式Exclusive独占,只有一个线程能执行,如ReentrantLockShare共享,多个线程可同时执行,如Semaphore/CountDownLatch。AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock

以可重入锁ReentrantLock 为例:state初始0,表示锁没有锁定。线程A调用lock()后,尝试用tryAcquire()方法独占锁并state+1。若成功A就获取到了锁,失败的话就加入等待队列中,等待其他线程释放锁。

以倒计时器 CountDownLatch 为例:任务分为N个子线程执行,state初始为N。N个子线程执行任务,每执行完一个子线程就调用一次countDown()方法。countDown()尝试用cas操作让state减1。所有子线程执行完毕后state变为0。CountDownLatch 调用unpark()方法唤醒主线程,主线程从await()方法返回继续执行后续操作。

**AQS性能比较好的原因是什么?**AQS 里使用了 CAS + 线程阻塞/唤醒CAS 基于内存地址直接进行数据修改,保证并发安全的同时,性能也很好。当 CAS 没有成功获取资源时,会对线程进行阻塞,避免一直空转占用 CPU 资源。

**AQS 中为什么 Node 节点需要不同的状态?**AQS通过不同的waitStatus值来控制状态流转,新节点加入队列时前继节点状态由 0 更新为 SIGNAL(-1) ,表示前序节点释放锁时需要对后继节点以进行唤醒操作;节点获取锁失败会变为 CANCELLED(1),此状态节点异常不能被唤醒也不能唤醒后继节点。

3. Semaphore是什么?什么作用?

Semaphore 是信号量,一种同步器。作用是控制同时访问共享资源的线程数量,通常用于那些资源有明确访问数量限制的场景。

Semaphore的原理:

Semaphore内部有个Sync类的变量,Sync继承了AQS抽象类。Semaphore默认构造AQS的state值为permits,许可证数量。

调用semaphore.acquire() ,线程尝试获取许可证,如果 state >= 0 ,则表示可以获取成功,使用 CAS 操作去修改 state 的值 state=state-1。如果 state<0 ,则表示许可证数量不足。此时会创建一个 Node 节点加入阻塞队列,挂起当前线程。

调用semaphore.release() ,线程尝试释放许可证,并使用 CAS 操作去修改 state 的值 state=state+1。释放许可证成功之后,同时会唤醒同步队列中的一个线程。被唤醒的线程会重新尝试去修改 state 的值 state=state-1 ,如果 state>=0 则获取令牌成功,否则重新进入阻塞队列,挂起线程。

4 CountDownLatch是什么?什么用?

CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用。

适用场景:我们要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。定义一个线程池和 count 为 6 的CountDownLatch对象 。使用线程池处理读取任务,每一个线程处理完之后就将 count-1,调用CountDownLatch对象的 await()方法,直到所有文件读取完之后,才会接着执行后面的逻辑。

这种场景也可以用CompletableFuture 类来改进,allOf方法。

5. CyclicBarrier 是什么?什么用?

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。

**作用:**让一组线程到达一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障时,所有被屏障拦截的线程才会继续工作。

**原理:**两个变量,parties时每次拦截的线程数,count是计数器。计数器count初始值等于 parties 的初始化值。每个线程调用 await () 方法时,计数器减 1,当 count 值为 0 时,表示这是这一代最后一个线程到达栅栏,就尝试执行构造方法中输入的任务。它通过 ReentrantLock 进行加锁控制,当线程中断或其他异常情况时会进行相应处理,并且当所有线程通过栅栏后会重置 count 并唤醒之前等待的线程,以便下一波执行开始。

CyclicBarrierCountDownLatch的区别?CountDownLatch 是让一个或多个线程等待其他线程完成一组操作后再继续,计数器不能重置;而 CyclicBarrier 是让一组线程相互等待,到达屏障点后可以重置继续使用。CountDownLatch 的实现是基于 AQS 的,而 CycliBarrier 是基于 ReentrantLock(ReentrantLock 也属于 AQS 同步器)和 Condition 的。

ReentrantLock类

ReentrantLock 实现了 Lock 接口,是一个可重入且独占式的锁。

ReentrantLock 有一个内部类 SyncSync 继承 AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际在 Sync 中实现。

1. 公平锁和非公平锁有什么区别?

公平锁:锁被释放后,先申请锁的线程先获取到锁。先来后到保证时间顺序的公平,但是可能上下文切换频繁,性能差。

非公平锁:锁被释放后,后申请锁的线程可能会先获取到锁,按照随机顺序或者其他优先级,性能更好,但有的线程可能一直拿不到锁。

2. ⭐synchronized和ReentrantLock有什么区别?

相同:

  • 都是可重入锁。
    • 也叫递归锁。如果线程获得了某个对象的锁,对象锁没释放时还能再次获取,如果不可重入就会死锁。

区别:

  • synchronized依赖于JVM,ReentrantLock依赖于API

    • synchronized 是依赖于 JVM 实现的,虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。
    • ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
  • ReentrantLocksynchronized增加了一些高级功能,ReentrantLock等待可中断,可以指定公平锁或非公平锁,可以选择性通知,支持超时。

    • ReentrantLock可中断锁synchronized不可中断锁。lock.lockInterruptibly()会让获取锁的线程在阻塞等待的过程中可以响应中断,如果其他线程中断当前线程「 interrupt() 」,当前线程就会抛出 InterruptedException 异常,可以捕捉该异常进行相应处理。

    • ReentrantLock可以通过ReentrantLock(boolean fair)构造方法指定是公平锁还是非公平锁。而synchronized只能是非公平锁

    • 选择性通知,锁可以绑定多个条件。synchronized关键字与wait()notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。

      Lock对象中可以创建多个Condition接口的实例。线程对象可以注册在指定的Condition中,可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”。synchronized关键字就相当于整个 Lock 对象中只有一个Condition实例,所有的线程都注册在它一个身上,执行notifyAll()方法的话就会通知所有处于等待状态的线程,而Condition实例的signalAll()方法,只会唤醒注册在该Condition实例中的所有等待线程。

    • 支持超时ReentrantLock 提供了 tryLock(timeout) 的方法,可以指定等待获取锁的最长等待时间,如果超过了等待时间,就会获取锁失败,不会一直等待。好处:防止死锁、提高响应速度、处理时间敏感的操作。

Atomic原子类

1. 介绍一下Atomic原子类?

原子类指的是具有原子操作特性的类,原子操作是说一个操作要么完整执行要么不执行。java中的Atomic 原子类在java.util.concurrent.atomic 包中。

volatile 主要保证内存可见性,不保证原子性操作;而 Atomic 类既保证内存可见性(内部有使用volatile)又保证原子性(cas算法实现)操作。

2. JUC包中的原子类有哪几类?
  1. 基本类型原子类,使用原子的方式跟更新基本类型。AtomicIntegerAtomicLongAtomicBoolean

  2. 数组类型原子类,使用原子的方式更新数组里的某个元素。AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray

  3. 引用类型原子类,使用原子的方式更新引用类型。AtomicReferenceAtomicStampedReferenceAtomicMarkableReference

    AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

  4. 对象属性修改类型原子类,原子地更新某个类里的某个字段。AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicReferenceFieldUpdater原子更新引用类型里的字段

3. 说说 Atomic 类在实际项目中的常见应用场景

比如在统计并发访问量、实现并发计数器等场景中可以用到 Atomic 类。

利用 Atomic 类来实现一个简单的并发计数器,可以定义一个 AtomicInteger,在需要计数的地方调用其递增方法即可实现简单的并发计数器。

ThreadLocal 类

1. ThreadLocal是什么,什么作用?

ThreadLocal 是 Java 中的一个类,提供了一种线程本地存储的机制。ThreadLocal 为每个线程提供了一个独立的变量副本,,每个线程可以独立地访问和修改自己的副本,不会影响其他线程,避免了多线程访问时的资源争用,确保了线程安全。

使用场景:

  • 数据库连接管理:每个线程可以有一个独立的数据库连接,避免了多个线程共享数据库连接的问题。
  • 用户会话信息:在 Web 应用中,可以使用 ThreadLocal 存储每个请求的会话信息。
  • 事务管理:例如,Spring 的事务管理会使用 ThreadLocal 来保存当前线程的事务上下文。
2. ⭐ThreadLocal的原理?

通过线程的ThreadLocalMap来存储每个线程特有的变量值。

每个Thread中都有一个ThreadLocalMapThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。

当前线程调用 ThreadLocal 类的 setget方法时,实际内部调用LocalMap类对应的 get()set()`方法。

public class Thread implements Runnable {
    //......
    //与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //用于存储需要跨线程传递的 ThreadLocal 值
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //......
}
/**
默认情况下Thread中这两个变量都是 null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际调用`set`或`get`时,内部调用的是LocalMap`类对应的 `get()`、`set()`方法。
*/
3. ⭐ThreadLocal 内存泄漏是怎么导致的?如何避免?

线程池中的核心线程会被循环使用,每个线程中对应的ThreadLocalMap被线程强引用。所以每个线程对应的ThreadLocalMap不能被GC自动回收。

ThreadLocalMap中包含一个Entry数组,Entry数组中含有多个KeyThreadLocalvalue为存储的数据的Entry对象,虽然Entry对象中的Key是弱引用,能够被GC自动回收,但是value却是强引用,不能被GC自动回收,所以,在线程池中使用ThreadLocal会存在内存泄露的风险。

ThreadLocal对象存储在每个Thread线程内部的ThreadLocalMap中,并且在ThreadLocalMap中有一个Entry数组,Entry数组中的每一个元素都是一个Entry对象。每个Entry对象中存储着一个ThreadLocal对象与其对应的value值,每个Entry对象在Entry数组中的位置是通过ThreadLocal对象的threadLocalHashCode计算出来的,以此来快速定位Entry对象在Entry数组中的位置。所以,在Thread中,可以存储多个ThreadLocal对象。

阿里二面:ThreadLocal内存泄露灵魂四问,人麻了!-腾讯云开发者社区-腾讯云

如何避免?

  • 在使用完 ThreadLocal 后,调用 remove() 方法,从而显示地移除对应entry。
  • 线程池场景下,使用 try-finally 块可以确保即使发生异常,remove() 方法也一定会被执行。
4. 如何跨线程传递 ThreadLocal 的值?
  • InheritableThreadLocalThreadLocal 的子类,它允许子线程继承父线程中 ThreadLocal 的值。当在父线程中设置了 InheritableThreadLocal 的值后,新创建的子线程可以获取到相同的值。但是无法支持线程池场景下的 ThreadLocal 值传递。

  • TransmittableThreadLocalTransmittableThreadLocal (简称 TTL) 是阿里巴巴开源的工具类,继承并加强了InheritableThreadLocal类,可以在线程池的场景下支持 ThreadLocal 值传递。

TransmittableThreadLocal

阿里巴巴无法改动 JDK 的源码,因此他内部通过 装饰器模式 在原有的功能上做增强,以此来实现线程池场景下的 ThreadLocal 值传递。

Future 泛型接口

1. 什么是Future?有什么作用?

interface Future<V> 是 Java 并发编程中的一个接口,它代表一个异步计算的结果。

**作用:**让程序在执行异步任务的同时继续执行其他操作,提高程序的效率和响应性。发起一个异步任务后,通过 Future 有下面几个功能:

  • Future 接口的 5 个方法:
    • boolean cancel(boolean mayInterruptIfRunning):尝试取消执行任务。
    • boolean isCancelled():判断任务是否被取消。
    • boolean isDone():判断任务是否已经被执行完成。
    • get():等待任务执行完成并获取运算结果。
    • get(long timeout, TimeUnit unit):多了一个超时时间。

Future 的常见实现是 FutureTask,它既可以作为 Runnable 任务执行,也可以作为 Callable 任务执行,后者可以返回一个结果。

2. Future 和 Callable 有什么关系?

Callable 是一个接口,它定义了一个可以返回结果的异步任务。

通常将 Callable 的实现提交给线程池等执行环境来执行异步任务,而 Future 就是用来获取 Callable 执行结果的。Callable 的执行结果会封装在 Future 中。

例如,FutureTask 同时实现了 Future接口和Runnable 接口,FutureTask 有两个构造函数,可传入 Callable 或者 Runnable 对象。实际上,传入 Runnable 对象也会在方法内部转换为Callable 对象。FutureTask相当于对Callable 进行了封装,管理着任务执行的情况,存储了 Callablecall 方法的任务执行结果。

3. CompletableFuture是什么?与Future有什么关系?

CompletableFuture Future 的增强版。

它不仅具备 Future 的基本功能,如获取异步任务结果、检查任务状态等,还提供了异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)、函数式编程等能力。

public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
}

CompletableFuture 同时实现了 FutureCompletionStage 接口。CompletionStage 接口描述了一个异步计算的阶段。很多计算可以分成多个阶段或步骤,此时可以通过它将所有步骤组合起来,形成异步计算的流水线。

4. ⭐一个任务需要依赖另外两个任务执行完之后再执行,怎么设计?

通过CompletableFuture实现。.allOf().thenRunAsync()

// T1
CompletableFuture<Void> futureT1 = CompletableFuture.runAsync(() -> {
    System.out.println("T1 is executing. Current time:" + DateUtil.now());
    // 模拟耗时操作
    ThreadUtil.sleep(1000);
});
// T2
CompletableFuture<Void> futureT2 = CompletableFuture.runAsync(() -> {
    System.out.println("T2 is executing. Current time:" + DateUtil.now());
    ThreadUtil.sleep(1000);
});

// 使用allOf()方法合并T1和T2的CompletableFuture,等待它们都完成
CompletableFuture<Void> bothCompleted = CompletableFuture.allOf(futureT1, futureT2);
// 当T1和T2都完成后,执行T3
bothCompleted.thenRunAsync(() -> System.out.println("T3 is executing after T1 and T2 have completed.Current time:" + DateUtil.now()));
// 等待所有任务完成,验证效果
ThreadUtil.sleep(3000);
5. ⭐使用 CompletableFuture,有一个任务失败,如何处理异常?
  • 使用 whenComplete 方法可以在任务完成时触发回调函数,并正确地处理异常,而不是让异常被吞噬或丢失。
  • 使用 exceptionally 方法可以处理异常并重新抛出,以便异常能够传播到后续阶段,而不是让异常被忽略或终止。
  • 使用 handle 方法可以处理正常的返回结果和异常,并返回一个新的结果,而不是让异常影响正常的业务逻辑。
  • 使用 CompletableFuture.allOf 方法可以组合多个 CompletableFuture,并统一处理所有任务的异常,而不是让异常处理过于冗长或重复。
6. ⭐使用 CompletableFuture 的时候为什么要自定义线程池?

CompletableFuture 默认使用全局共享的 ForkJoinPool.commonPool() 作为执行器,导致没有指定执行器的异步任务共享一个线程池。当同时提交大量任务时,可能会导致资源竞争和线程饥饿,进而影响系统性能。

CompletableFuture 提供自定义线程池,带来以下优势:

  • 隔离性:为不同任务分配独立的线程池,避免全局线程池资源争夺。
  • 资源控制:根据任务特性调整线程池大小和队列类型,优化性能表现。
  • 异常处理:通过自定义 ThreadFactory 更好地处理线程中的异常情况。

网站公告


今日签到

点亮在社区的每一天
去签到