跳转至

JUC

一、基础知识

0. 什么是同步、什么是互斥?⭐⭐⭐

  • 互斥是指多个线程访问共享资源时,同一时刻只能有一个线程访问,避免数据竞争。
  • 同步是指多个线程之间按照特定的执行顺序去执行,使线程能够有序地完成任务

1. Java 线程和操作系统线程有什么区别?⭐⭐⭐

现在的 Java 线程(JDK 1.2 至今)本质上就是操作系统的原生线程(Native Threads)。 Java 采用的是 1:1 线程模型,即:每一个 Java 线程(Thread 实例)都会对应一个操作系统的内核线程(Kernel Thread)。线程的创建、销毁、调度和切换全部交由操作系统内核负责。可以利用多核并行,但创建和切换成本较高。

Java 21 引入的虚拟线程由 JVM 调度多个虚拟线程可以复用一个操作系统线程,创建和切换成本更低,适合大量 I/O 阻塞任务。

2. 线程与进程有什么区别呢?⭐⭐⭐

进程:操作系统分配资源(内存、IO)的最小单位。进程间相互独立,但如果直接对进程进行上下文切换,则开销是比较大的,因此,为了提高操作系统的并发能力,又引入了线程!

线程:是操作系统调度和执行的最小单位。

同一进程中的多个线程共享进程的堆方法区等资源,但各自拥有独立的程序计数器虚拟机栈本地方法栈。线程的上下文切换和通信成本通常低于进程。

3. 什么是上下文切换?⭐⭐⭐

上下文切换是 CPU 从一个线程切换到另一个线程时,保存当前线程的执行状态并恢复另一个线程执行状态的过程,会产生一定的性能开销。

常见触发场景包括时间片用完、线程阻塞、主动让出 CPU 和线程执行结束。

4. 为什么 wait() 方法不定义在 Thread 中?(了解,这问题有点神奇)

因为 wait() 等待的是某个对象的锁状态,而不是等待某个线程。Java 中任何对象都可以作为锁。因此,wait()notify()notifyAll() 定义在 Object 类中。

sleep() 只是让当前线程暂停执行,不涉及对象锁,因此定义在 Thread 类中。

5. 可以直接调用 Thread 类的 run 方法吗?⭐⭐⭐

可以,但直接调用 run() 只是普通方法调用,不会创建新线程;调用 start() 才会创建并启动一个新线程,随后由新线程执行 run() 方法。

6. 并发容器

  • CopyOnWriteArrayList:适合读多写少的场景,写操作会复制底层数组。
  • ConcurrentHashMap:通过 CAS、synchronized 等机制保证并发安全
  • BlockingQueue:支持阻塞读写,常用于生产者消费者模型

7. 操作系统进程调度算法有哪些?什么是进程调度 ⭐⭐⭐

进程调度:操作系统按照某种策略,从就绪队列中选择一个进程,将 CPU 分配给它的过程。

常见调度算法

  • 先来先服务(FCFS):按到达顺序执行,简单但可能导致" convoy 效应"(短任务等长任务)。
  • 短作业优先(SJF):优先执行运行时间短的任务,平均等待时间最短,但无法预知执行时间。
  • 优先级调度:按优先级高低执行,可能导致低优先级任务"饥饿"(可用"老化"机制解决)。
  • 时间片轮转:每个进程分配一个时间片,轮流执行,公平但时间片大小的选择是关键。
  • 多级反馈队列:综合以上优点,动态调整优先级和时间片,现代 OS 常用。

8. 有哪些页面置换算法,什么是页面置换算法?⭐⭐⭐

页面置换:当物理内存不足时,操作系统选择一个页面换出到磁盘,腾出空间给新页面的过程。

常见算法

  • OPT(最佳置换):淘汰未来最久不用的页面,理论最优但无法实现(无法预知未来)。
  • FIFO(先进先出):淘汰最早进入的页面,简单但可能淘汰常用页面,还存在 Belady 异常(分配更多内存反而缺页增多)。
  • LRU(最近最少使用):淘汰最近最久未使用的页面,效果好但实现开销大,是实际应用中最常用的算法。
  • 时钟置换(Clock):LRU 的近似算法,用环形链表 + 访问位,性能接近 LRU 且实现简单。

一句话总结:OPT 理论最优,LRU 实际最优,Clock 是 LRU 的工程折中。

二、JUC 辅助类

1. 什么是 CountDownLatch?⭐⭐⭐

CountDownLatch 是一种线程同步工具,它实现了一个或一组线程等待另一组线程任务执行完成后的等待关系。

初始化时设置计数器,子任务完成后调用 countDown() 将计数器减一,等待线程调用 await(),直到计数器减为 0 后继续执行。

2. CountDownLatch 的计数器可以重置吗?⭐

不能。CountDownLatch 的计数器只能递减,减到 0 后就失效了;如果需要循环使用,应使用 CyclicBarrier 或重新创建新的 CountDownLatch

3. 子线程抛出异常会导致主线程一直等待吗?⭐

看情况。如果子线程报错没能调用 countDown(),计数器永远不会归零。所以主线程也永远不会被唤醒

我们可以通过 try-catch-finally 或者在主线程使用 await() 阻塞自己时,设置一个最大超时时间来避免无限等待。

4. CountDownLatch 和 Join 的区别?⭐⭐⭐

join 是等“线程” 运行结束、销毁掉才算完;CountDownLatch 是等“计数器为0”,只要任务做完了(计数器归零),不管线程死没死,主线程都能接着干。

“所以在实际开发中,CountDownLatch 配合线程池非常方便(因为线程池里的线程是通常是不死的),而 join 很难跟线程池配合。”

5. CountDownLatch 基于什么实现?

CountDownLatch 基于 AQS 的共享模式实现,使用 AQS 的 state 保存计数器值;await() 在计数器不为 0 时等待,countDown() 将计数器减一,减到 0 后唤醒等待线程。

三、Callable 接口

1. CallableRunnable 有什么区别?⭐⭐⭐

Callable 可以有返回值、可以抛出异常;Runnable 没有返回值,不能直接抛出受检异常。

Callable 通常配合 FutureFutureTask 获取异步任务的执行结果

2. futureTask.get() 放在哪执行?

通常应先启动所有异步任务,最后再调用 get() 获取结果,因为 get() 在任务未完成时会阻塞当前线程。

3. 多个线程启动同一个 FutureTask 对象,任务会执行几次?

只会执行一次FutureTask 内部有个状态位,一旦任务执行完成或正在执行,后续的调用会直接拿结果。

juc01

public V get() throws InterruptedException, ExecutionException {  
    int s = state;  
    if (s <= COMPLETING)  
        s = awaitDone(false, 0L);  
    return report(s);  
}

4. 如何实现异步不阻塞?

可以通过 isDone() 轮询检查,或者干脆直接学习 JDK 8 的 CompletableFuture

“实现异步不阻塞,关键在于把‘主动索取结果’改为‘被动接收回调’CompletableFuture 通过异步编排,将多个任务串成流水线。主线程只负责开启这条流水线,任务执行完会自动触发后续操作,从而彻底解放主线程,避免了阻塞死等。”

四、Volatile 关键字⭐️

1. Volatile 如何实现变量的可见性?(原理)⭐️⭐⭐

在 Java 中,如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主内存中进行读取。并且在线程的工作内存修改后,应该立即更新到主内存,而非写入缓存。

2. 线程本地内存的理解

其实就是 cache 或者寄存器。

3. 如何禁止指令重排序⭐️

在 Java 中,volatile 关键字除了可以保证变量的可见性,还有一个重要的作用就是防止 JVM | os 的指令重排序。 如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。

4. 双重检验锁方式实现案例模式⭐️

synchronized 虽然保证了原子性,但无法禁止指令重排。在 DCL 中,new 对象并非原子操作,重排可能导致‘引用先指向地址对象后初始化’。此时如果另一个线程在锁外进行 null 检查,会误判对象已就绪,从而拿到一个未初始化的半成品。加 volatile 就是为了通过内存屏s障禁止这种重排,确保安全。”

public class Singleton {

    private volatile static Singleton uniqueInstance;
s
    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

5. 为什么要进行两次双重检验呢?

第一层 if:避免对象创建后,每次获取实例都要进行昂贵的加锁操作。

第二层 if内层检查是为了原子性/安全。防止多个线程同时通过了外层检查,在排队等待锁的过程中产生重复创建对象的行为。

6. Volatile 可以实现原子性吗?⭐⭐⭐

显然不能,Volatile 只是实现了共享变量的可见性,而无法保证原子性,想象:两个线程从主内存中同时拿到了副本,并且都要进行 ++ 操作,此时就会出现:这两个线程的写结果相互覆盖。

五、悲观锁和乐观锁

1. 什么是悲观锁?⭐⭐⭐

悲观锁总是悲观的认为:多个线程同时访问共享资源就会出现一些并发问题,所以每次操作共享资源的时候都会上锁,保证同一时刻只能有一个线程访问共享资源。

像 Java 中synchronizedReentrantLock 等独占锁就是悲观锁思想的实现。

2. 什么是乐观锁?⭐⭐⭐

乐观锁是一种并发控制思想,它认为大多数情况下并发访问数据不会发生冲突,因此不会提前加锁,而是在更新数据时检查数据是否被其他线程修改过

如果数据没有被修改,则更新成功;如果发现数据已经被其他线程修改,则放弃更新或者进行重试。

3. 版本号机制

通过一个 版本号字段 来表示数据被修改的次数,因此,每次线程 A 操作共享数据时,就可以通过版本号来判断是否有其他线程修改了当前共享资源,比较典型的就是 java 集合中 modCount 字段。

4. CAS 算法⭐️⭐⭐

CAS 的全称是 Compare And Swap(比较与交换) ,用于实现乐观锁,CAS 的思想很简单,就是主内存中的值与旧值比较,如果相同,则没被其他线程修改,则可以把新值写入到主内存,否则自旋 + 重试。

  • V:当前值。
  • A:预期值。
  • B:准备更新的新值。

只有当 V == A 时,CAS 才会将 V 更新为 B;否则更新失败,程序通常会重试或取消更新

5. CAS 有什么问题?⭐⭐⭐

  • ABA 问题:值从 A 改为 B,再改回 A,CAS 只能看到最终值仍是 A,无法判断中间是否发生过修改。可以使用带版本号的原子类,例如 AtomicStampedReference
  • 自旋开销:竞争激烈时,线程会反复重试,持续占用 CPU。
  • 适用范围有限:CAS 更适合简单的单变量原子更新,复杂的多变量一致性操作通常需要使用锁或其他同步机制。

六、Synchronized 关键字

1. Synchronized 是什么,有什么用⭐️⭐⭐

synchronized 是 Java 提供的同步机制,用于保证同一时刻只有一个线程执行被保护的代码或者方法,从而保证临界区操作的原子性,并保证锁释放前的修改对随后获取同一把锁的线程可见。

2. synchronized 有哪些使用方式?⭐

// 修饰实例方法:锁当前对象
public synchronized void method() {
}

// 修饰静态方法:锁当前 Class 对象
public static synchronized void staticMethod() {
}

// 修饰代码块:锁指定对象
synchronized (lock) {
}
````

不同对象对应不同的锁只有使用**同一个锁对象**多个线程之间才具有互斥效果

#### 3. `synchronized` 是可重入锁吗?⭐

持有锁的线程再次进入同一个锁保护的代码时可以直接通过不需要重新获取锁退出相应次数后锁才会真正释放

#### 4. 演化过程了解

- **早期重量级**依赖操作系统的 `Mutex Lock`。线程切换需要从 **用户态** 切换到 **内核态**代价极高效率低下
- **优化Java 6+**引入了**锁升级**机制偏向锁 $\rightarrow$ 轻量级锁 $\rightarrow$ 重量级锁),让锁能根据竞争情况自动变身”,不再动不动就找操作系统

#### 5. `synchronized` 做了哪些优化?⭐⭐⭐

JVM 会根据**锁竞争情况**选择不同的**加锁方式**尽量避免直接将线程挂起

- **轻量级锁**适合多个线程交替获取锁竞争不激烈的场景线程会先通过 CAS 尝试获取锁如果锁很快被释放**线程不需要进入阻塞状态**
- **自适应自旋**线程获取锁失败后先在一段时间内循环等待而不是立即挂起如果预计锁很快释放自旋可以减少线程切换开销如果竞争激烈线程最终仍会进入阻塞状态
- **锁消除**JIT 编译器判断某个锁不存在**实际竞争时**直接删除加锁操作
- **锁粗化**如果一段代码**连续多次**对同一个对象加锁和解锁**JIT 编译器可能扩大锁的范围**减少**加锁和解锁**次数

简单来说轻量级锁和自适应自旋适合短时间低竞争的场景竞争激烈时线程最终可能升级为重量级锁并进入阻塞

#### 6. 构造方法可以使用 `synchronized` 修饰吗

不能Java 语法不允许使用 `synchronized` 修饰构造方法如果构造过程中需要保护共享资源应在构造方法内部对指定对象加锁

#### 7. synchronized 的底层原理⭐️⭐⭐

“`synchronized` 底层是基于**监视器锁**Monitor实现的代码块通过 `monitorenter/exit` 指令实现方法通过 `ACC_SYNCHRONIZED` 标志实现

其本质是线程尝试获取对象头所关联的 Monitor 对象的持有权如果获取成功Monitor  `_Count`  1如果失败线程就会进入阻塞状态直到锁被释放。”

所以,`wait()`、`notify()`  `notifyAll()` 这些方法本质上是** Monitor 内部队列的操作**因此只有线程成为了当前 monitor 的主人才能进行操作而进入 `synchronized` 代码块就代表着当前线程成为了 monitor 对象的主人

#### 8. 什么是 monitor

每个 Java 对象出生时都会带一个**隐形锁**”,也就是 Monitor 对象 C++ 实现)。它内部的关键字段有

- **`_Owner`**当前是谁占着这把锁存的是线程 ID)。
- **`_Count`**锁被重入了多少次为了支持**可重入性**)。
- **`_WaitSet`**那些调用了 `wait()` 的线程在这儿休息
- `_EntryList`:等待获取锁而阻塞的线程队列

``` cpp
ObjectMonitor() {
    _header       = NULL;
    _count        = 0;     // 记录锁的重入次数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;  // 指向持有 ObjectMonitor 对象的线程
    _WaitSet      = NULL;  // 调用 wait() 后等待的线程队列
    _EntryList    = NULL;  // 等待获取锁而阻塞的线程队列
}

8. 那锁存在哪里呢?

synchronized 的锁信息主要记录在对象头的 Mark Word 中;当锁竞争升级为重量级锁后,Mark Word 会关联到对应的 Monitor 对象。

9. synchronized 和 volatile 有什么区别?

synchronized 通过加锁保证代码块的互斥执行、实现原子性和可见性,可能导致线程阻塞volatile 只能保证变量的可见性和一定的有序性,不能保证复合操作的原子性,但不会阻塞线程,适合状态标记等简单共享变量。

七、ReentrantLock

1. ReentrantLock 是什么?⭐⭐⭐

ReentrantLock 实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。不过,ReentrantLock 更灵活、更强大,增加了轮询超时中断、公平锁和非公平锁等高级功能

2. ReentrantLock 的底层原理是什么?⭐

ReentrantLock 基于 AQS 的互斥模式实现,使用 state 表示锁状态,并通过 CAS 尝试获取锁;获取失败的线程会进入等待队列,必要时被挂起,锁释放后再被唤醒。

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

synchronized 是托管给 JVM 执行的“自动挡”锁,简单省心;ReentrantLock 是基于 API 实现的“手动挡”锁,功能更强、控制更细。

4. 什么是可中断获取锁?⭐

使用 lockInterruptibly() 获取锁时,如果线程在等待锁的过程中被中断,就会放弃等待并抛出 InterruptedException;普通 lock() 获取锁时不会响应中断。

5. tryLock() 有什么作用?⭐

tryLock() 会尝试获取锁,获取不到时立即返回;tryLock(timeout, unit) 可以等待指定时间,超时后放弃获取,从而避免线程无限阻塞

6. 如何排查 Java 死锁?⭐

可以先使用 jps -l 获取 Java 进程 ID,再使用 jstack -l <PID> 查看线程栈,或者使用 JConsole、VisualVM 等工具分析死锁。

八、ReentrantReadWriteLock

ReentrantReadWriteLock 是一种可重入的读写锁,允许多个线程并发读取,但写操作需要独占锁,适合读多写少的场景。

1. 为什么要有读写锁 ⭐⭐⭐

ReentrantLock 会限制所有并发访问,包括读操作之间的并发;读写锁允许多个线程同时读,只有写操作需要互斥,从而提高读多写少场景下的并发性能。

2. 什么是共享锁、什么是独占锁

  • 共享锁:同一时刻可以被多个线程持有,读锁属于共享锁。
  • 独占锁:同一时刻只能被一个线程持有,写锁、ReentrantLocksynchronized 都属于独占锁。

3. 读写锁的获取规则

当线程持有读锁时,就无法获取写锁;当写锁没有被线程占有时,那么所有线程都可以拿到读锁;如果当前写锁被占了,那么之后的线程无论是读锁,还是写锁,都无法获取。

4. 锁降级机制

锁降级是指线程持有写锁时先获取读锁,再释放写锁,之后继续持有读锁。

这样可以保证写操作完成后,当前线程能够继续读取刚写入的数据,中间不会被其他写线程插入修改。

5. 为什么读锁不能升级为写锁呢?

读锁升级为写锁容易导致死锁:线程持有读锁后申请写锁时,需要等待其他读线程释放读锁;如果其他读线程也在等待升级写锁,就会互相等待。实际开发中通常应先释放读锁,再重新申请写锁。

八、ThreadLocal

ThreadLocal 为每个线程提供了一个专属的本地变量,让每个线程只能操作自己的数据,从而在逻辑上绕开了并发竞争。

1. ThreadLocal 有什么用?⭐⭐⭐

通常情况下,我们创建的变量可以被任何一个线程访问和修改。这在多线程环境中可能导致数据竞争和线程安全问题。那么,如果想让每个线程都有自己的专属本地变量,该如何实现呢?

ThreadLocal 为每个线程提供独立的变量副本,使线程之间的数据互不干扰,常用于保存当前用户、数据库连接、请求上下文等线程隔离数据。

所以,ThreadLocal 最大的用处就是:实现线程间的数据隔离,让每个线程都有自己的专属副本,避免了加锁带来的性能损耗。

2. ThreadLocal 原理

每个线程内部都有一个 ThreadLocalMap,用于保存当前线程的 ThreadLocal 数据:

Thread
  └── ThreadLocalMap
        ├── ThreadLocal -> value
        └── ThreadLocal -> value

调用 set() 时,数据实际上保存在线程自己的 ThreadLocalMap 中;调用 get() 时,再根据当前的 ThreadLocal 对象取出对应的值。

因此,不同线程调用同一个 ThreadLocal 时,获取到的是各自独立的值。

3. ThreadLocal 为什么可能发生内存泄漏?⭐⭐⭐

ThreadLocalMap 中的 Key 是对 ThreadLocal 的弱引用,但 Value 是强引用。

ThreadLocal 对象没有其他强引用时,Key 可能被垃圾回收,但 Value 仍会暂时保留在线程的 ThreadLocalMap 中。如果线程长期存活,例如线程池中的工作线程,就可能导致 Value 长期占用内存。

4. 如何避免内存泄漏⭐️⭐⭐

在使用完 ThreadLocal 后,务必调用 remove() 方法。 这是最安全和最推荐的做法。 remove() 方法会从 ThreadLocalMap 中显式地移除对应的 entry,彻底解决内存泄漏的风险。 即使将 ThreadLocal 定义为 static final,也强烈建议在每次使用后调用 remove()

5. 如何跨线程传递 ThreadLocal 的值呢?

由于 ThreadLocal 的变量值存放在 Thread 里,而父子线程属于不同的 Thread 的。因此在异步场景下,父子线程的 ThreadLocal 值无法进行传递。

如果想要在异步场景下传递 ThreadLocal 值,有两种解决方案:

  • InheritableThreadLocalInheritableThreadLocal 是 JDK1.2 提供的工具,继承自 ThreadLocal 。使用 InheritableThreadLocal 时,会在创建子线程时,令子线程继承父线程中的 ThreadLocal 值,但是无法支持线程池场景下的 ThreadLocal 值传递。
  • TransmittableThreadLocalTransmittableThreadLocal (简称 TTL) 是阿里巴巴开源的工具类,继承并加强了InheritableThreadLocal类,可以在线程池的场景下支持 ThreadLocal 值传递。项目地址:https://github.com/alibaba/transmittable-thread-local

需要注意:

  • ThreadLocal 的 Key 是弱引用,但 Value 不是弱引用。
  • 弱引用只能帮助 Key 被回收,不能保证 Value 立即被清理,因此仍然需要手动 remove()
  • ThreadLocal 适合线程隔离数据,不适合在线程之间共享数据。
  • InheritableThreadLocal 是在线程创建时传递值,不是每次提交任务时传递值

九、ThreadPool

1. 什么是线程池

顾名思义,线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。

2. 为什么使用线程池 ⭐️⭐⭐

使用线程池的核心目的就是减少线程的创建和销毁的开销,从而节省资源,同时提高任务的响应速度。其次,线程池能够更好的帮助我们去管理一系列线程。

3. 如何创建线程池 ⭐⭐

推荐直接使用 ThreadPoolExecutor,明确指定线程数量、任务队列和拒绝策略;不建议在生产环境直接使用 Executors 创建线程池,因为部分实现使用无界队列或可能创建大量线程,存在内存溢出风险。

4. 线程池七大参数 ⭐⭐⭐

public ThreadPoolExecutor(int corePoolSize,  
                          int maximumPoolSize,  
                          long keepAliveTime,  
                          TimeUnit unit,  
                          BlockingQueue<Runnable> workQueue,  
                          ThreadFactory threadFactory,  
                          RejectedExecutionHandler handler)
  • corePoolSize:核心线程数。
  • maximumPoolSize:最大线程数。
  • keepAliveTime:非核心线程空闲后的存活时间。
  • TimeUnit unit:非核心线程空闲后的存活时间单位。
  • workQueue:暂存等待执行任务的阻塞队列。
  • threadFactory:创建线程的工厂,可以设置线程名称。
  • handler:任务无法处理时的拒绝策略。

5. 线程池的处理流程⭐️⭐⭐

6. 线程池有哪些拒绝策略?⭐⭐⭐

  • AbortPolicy:抛出异常,默认策略。
  • CallerRunsPolicy:由提交任务的线程执行任务,降低任务提交速度。
  • DiscardPolicy:直接丢弃新任务,不抛出异常。
  • DiscardOldestPolicy:丢弃队列中最早的任务,再尝试提交新任务。

7. 为什么不建议使用 Executors 创建线程池?⭐

FixedThreadPoolSingleThreadExecutor 使用近似无界的任务队列,任务堆积可能导致内存溢出;CachedThreadPool 允许创建大量线程,在高并发下可能耗尽系统资源。

8. execute()submit() 有什么区别?⭐⭐

execute() 只能提交 Runnable,没有返回值,任务异常通常会直接抛出;submit() 可以提交 RunnableCallable,会返回 Future,任务异常会被封装,调用 Future.get() 时才会抛出。

9. 如何设置线程池大小?⭐⭐⭐

CPU 密集型任务的线程数通常接近 CPU 核心数(通常 N + 1);I/O 密集型任务可以设置更多线程,但具体数量还要根据任务的阻塞时间、CPU 核数、队列长度和压测结果确定,不能机械地固定为 N + 12N

10. 如何优雅地关闭线程池?⭐

先调用 shutdown() 拒绝新任务并等待已提交任务执行;在规定时间内未结束时,再调用 shutdownNow() 尝试中断任务,并正确处理 InterruptedException,恢复当前线程的中断状态。

十、Future

1. Future 类有什么用 ⭐⭐

Future 用于表示异步任务的结果,可以提交任务后继续执行其他操作,之后通过 get() 获取结果,也可以取消任务查询任务状态

注意:get() 在任务未完成时会阻塞当前线程,可以使用带超时时间的 get() 避免无限等待。

2.FutureTask 是什么

FutureTaskFutureRunnable 的实现类,可以封装 CallableRunnable 任务,既可以作为任务提交执行,也可以通过 get() 获取结果。

3. Future 有什么局限?CompletableFuture 解决了什么问题?⭐⭐

Future 只能通过阻塞式的 get() 获取结果,不方便实现多个异步任务之间的依赖组合和异常处理

CompletableFuture 支持回调链式编排任务组合和异常处理

CompletableFuture.supplyAsync(() -> "hello")
        .thenApply(String::toUpperCase)
        .thenAccept(System.out::println);

4. 一个任务依赖两个任务的结果,应该如何实现?⭐⭐

使用 CompletableFuture.allOf() 等待多个任务完成,再通过后续阶段执行依赖任务。

CompletableFuture<String> task1 =
        CompletableFuture.supplyAsync(() -> "result1");

CompletableFuture<String> task2 =
        CompletableFuture.supplyAsync(() -> "result2");

CompletableFuture.allOf(task1, task2)
        .thenRunAsync(() -> {
            String result1 = task1.join();
            String result2 = task2.join();
            // 使用两个任务的结果执行后续逻辑
        });

5. CompletableFuture 如何处理异常?⭐

  • exceptionally():发生异常时提供一个兜底结果。
  • handle():同时处理正常结果和异常,并返回新的结果。
  • whenComplete():无论成功或失败都执行回调,但不改变原有结果。

十一、AQS

1. AQS 是什么?⭐⭐⭐

AQS(AbstractQueuedSynchronizer)是 Java 提供的构建锁和同步器的抽象基础框架,核心是使用一个 state 表示同步状态,并使用一个同步队列管理获取资源失败的线程

ReentrantLockSemaphoreCountDownLatch 等工具都基于 AQS 实现。

2. AQS 的原理是什么?⭐⭐⭐

线程获取资源时,先通过 CAS 修改 state

  • 获取成功:线程继续执行。
  • 获取失败:线程被封装为 Node 加入 AQS 同步队列,并通过 LockSupport.park() 挂起。
  • 资源释放:修改 state,并唤醒同步队列中的后继线程。
  • 被唤醒的线程还需要重新竞争资源,竞争成功后才能继续执行。

AQS 支持两种资源获取模式:

  • 独占模式:同一时刻只能有一个线程获取资源,例如 ReentrantLock
  • 共享模式:多个线程可以同时获取资源,例如 SemaphoreCountDownLatch

3. Semaphore 有什么用呢?

Semaphore 是信号量,用于限制同时访问某个资源的线程数量。它适合连接池限流、并发访问控制等场景,并支持公平和非公平两种模式。

// 初始共享资源数量
final Semaphore semaphore = new Semaphore(5);
// 获取1个许可
semaphore.acquire();
// 释放1个许可
semaphore.release();

4. CountDownLatch 有什么作用?⭐

CountDownLatch 用于实现让一个或多个线程等待一组任务完成的等待关系。

初始化计数器后,任务完成时调用 countDown(),等待线程调用 await();计数器减为 0 后,等待线程继续执行。

5. CountDownLatchCyclicBarrier 有什么区别?⭐

CountDownLatch 用于让一个或多个线程等待其他任务完成,计数器只能使用一次;CyclicBarrier 用于一组线程相互等待,所有线程到达屏障后同时继续执行,并且可以重复使用。

6. 用过 CountDownLatch 么?什么场景下用的 ⭐⭐⭐

首先,countDownLatch 本质就是用来实现一组或者一个线程等待另一组或者另一个线程执行完后,再去执行的同步关系的,而这样的同步关系在我们的实际业务场景中是很常见的。

比如:多源数据汇总(最常见),在我之前项目:听书中,专辑详情信息的渲染需要调用其他多个微服务,比如用户微服务、声音微服务等等,那么此时主线程需要等待这些子线程执行完后才能封装最后的结果,然后返回,此时,就是一个经典的 countDownLatch 的应用场景,只不过 java 8 提供的 completableFuture 更加强大,因此一般能用 countDownLacth 实现的场景,我都会选择 CompletableFuture 。

7. CyclicBarrier 有什么作用?

public static void main(String[] args) {  
    CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> {  
        System.out.println("人到齐了");  
    });  
    for (int i = 0; i < 4; i++) {  
        new Thread(() -> {  
            try {  
                System.out.println(Thread.currentThread().getName() + "开始执行");  
                cyclicBarrier.await();  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
        }, "A" + i).start();  
    }  
}

CyclicBarrier 主要用来实现一组线程相互等待的同步关系,也就是这一组线程会在某个点互相等待对方,直至最后一个线程来到这个点,然后再去共同执行。

十二、虚拟线程

1. 虚拟线程是什么?

虚拟线程(Virtual Thread)是 JDK 而不是 OS 实现的轻量级线程(Lightweight Process,LWP),由 JVM 调度。许多虚拟线程共享同一个操作系统线程,虚拟线程的数量可以远大于操作系统线程的数量。

2. 虚拟线程适用场景?

虚拟线程适合大量 I/O 阻塞任务,例如网络请求、数据库访问和文件操作,可以提高系统吞吐量并简化异步代码。

它不适合用来提升 CPU 密集型任务的计算能力,因为 CPU 密集型任务最终仍受限于 CPU 核心数。

3. 如何创建虚拟线程

  • 使用 Thread.startVirtualThread() 创建
  • 使用 Thread.ofVirtual() 创建
  • 使用 ThreadFactory 创建
  • 使用 Executors.newVirtualThreadPerTaskExecutor()创建