Java 并发编程 #

高频面试核心:线程、锁、线程池、并发工具类


📋 目录 #


线程基础 #

线程创建方式 #

// 1. 继承Thread
class MyThread extends Thread {
    @Override
    public void run() {}
}

// 2. 实现Runnable
new Thread(() -> {}).start();

// 3. 实现Callable(有返回值)
FutureTask<String> future = new FutureTask<>(() -> "result");
new Thread(future).start();
String result = future.get();

// 4. 线程池
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> {});

线程状态转换 #

new Thread()

start()

获得CPU

时间片用完

synchronized/wait

wait/join/park

sleep/wait/timedPark

竞争到锁

notify/notifyAll/unpark

超时/唤醒

执行完毕

New

Runnable

Running

Blocked

Waiting

TimedWaiting

Terminated

状态 说明 触发条件
NEW 新建 new Thread()
RUNNABLE 可运行 start()
BLOCKED 阻塞 等待synchronized锁
WAITING 等待 wait()/join()/park()
TIMED_WAITING 计时等待 sleep()/wait(n)
TERMINATED 终止 执行完毕

锁机制 #

synchronized vs ReentrantLock #

特性 synchronized ReentrantLock
实现方式 JVM API (AQS)
锁类型 可重入锁 可重入锁
获取锁状态 ❌ 无法判断 tryLock()
释放锁 自动 必须 finally 释放
公平锁 ❌ 非公平 ✅ 公平/非公平
响应中断 lockInterruptibly()
Condition ❌ wait/notify ✅ 多个Condition

synchronized 锁升级 #

单线程

CAS竞争

挂起线程

无锁

偏向锁

轻量级锁

重量级锁

阶段 说明 场景
偏向锁 Mark Word存储线程ID 单线程重复执行
轻量级锁 CAS自旋竞争 低竞争场景
重量级锁 内核mutex锁 高竞争场景

⚠️ 锁只能升级不能降级

ReentrantLock 示例 #

ReentrantLock lock = new ReentrantLock(); // 非公平锁
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁

try {
    lock.lockInterruptibly(); // 可中断
    // 临界区
} finally {
    lock.unlock();
}

// 条件变量
Condition condition = lock.newCondition();
try {
    condition.await();
    condition.signal();
} finally {
    lock.unlock();
}

volatile 可见性 #

// 保证可见性 + 禁止指令重排序
private volatile boolean running = true;

// 底层实现
// 1. 内存屏障(Memory Barrier)
// 2. LOCK 前缀指令

特性:

  • ✅ 保证可见性(主存读写)
  • ✅ 禁止指令重排序
  • ❌ 不保证原子性

适用场景:

  • 状态标记位
  • 单例模式(DCL)
  • 简单读写

Atomic 原子类 #

用途 实现方式
AtomicInteger 整数原子操作 CAS + volatile
AtomicLong 长整数原子操作 CAS + volatile
AtomicReference 引用原子操作 CAS + volatile
AtomicStampedReference 带版本号的引用 ABA问题解决
AtomicIntegerArray 整数数组原子操作 数组+元素CAS

线程池 #

面试高频 ⭐⭐⭐⭐⭐

七大参数详解 #

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize,      // 1. 核心线程数
    maximumPoolSize,   // 2. 最大线程数
    keepAliveTime,     // 3. 空闲线程存活时间
    TimeUnit.SECONDS,  // 4. 时间单位
    workQueue,         // 5. 工作队列
    threadFactory,     // 6. 线程工厂
    handler            // 7. 拒绝策略
);

工作流程图 #

提交任务

核心线程
是否满?

创建核心线程执行

队列是否满?

任务入队

最大线程
是否满?

创建非核心线程

执行拒绝策略

参数详解 #

参数 默认值 说明
corePoolSize - 核心线程数,常驻不销毁
maximumPoolSize - 最大线程数
keepAliveTime 60s 非核心线程空闲超时时间
workQueue - 任务等待队列
threadFactory Default 线程创建工厂
handler Abort 饱和拒绝策略

常用队列 #

队列类型 特性 适用场景
ArrayBlockingQueue 有界数组 防止资源耗尽
LinkedBlockingQueue 无界链表 任务可无限堆积
SynchronousQueue 不缓存 立即传递给线程
PriorityBlockingQueue 优先级 任务有优先级

拒绝策略 #

策略 说明
AbortPolicy 抛出异常(默认)
CallerRunsPolicy 调用者线程执行
DiscardPolicy 直接丢弃
DiscardOldestPolicy 丢弃最老任务

四大创建方式对比 #

// 1. FixedThreadPool(固定大小)
// core=maximum,无界队列,keepAlive=0
ExecutorService fixed = Executors.newFixedThreadPool(10);
// ⚠️ OOM风险:队列无限制

// 2. CachedThreadPool(缓存)
// core=0, maximum=∞,SynchronousQueue
ExecutorService cached = Executors.newCachedThreadPool();
// ⚠️ 线程爆炸风险:无限制创建线程

// 3. SingleThreadPool(单线程)
// core=maximum=1,无界队列
ExecutorService single = Executors.newSingleThreadExecutor();

// 4. ScheduledThreadPool(定时)
// 支持定时和周期任务
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(10);

最佳实践 #

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4,                      // CPU密集: CPU核心数 + 1
    Runtime.getRuntime().availableProcessors() * 2,  // IO密集: 2*CPU核心数
    60L,
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000),  // 有界队列
    new ThreadFactoryBuilder().setNameFormat("pool-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略
);

// 优雅关闭
executor.shutdown();
try {
    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
        executor.shutdownNow();
    }
} catch (InterruptedException e) {
    executor.shutdownNow();
}

并发工具 #

CountDownLatch 门闩 #

// 6个线程,等待所有完成
CountDownLatch latch = new CountDownLatch(6);

for (int i = 0; i < 6; i++) {
    new Thread(() -> {
        // 执行任务
        latch.countDown(); // 计数减1
    }).start();
}

latch.await(); // 等待计数为0
System.out.println("所有任务完成");

应用场景:

  • 多任务并行后汇总结果
  • 并发测试

CyclicBarrier 循环栅栏 #

// 7个线程,到齐后一起执行
CyclicBarrier barrier = new CyclicBarrier(7, () -> {
    System.out.println("所有线程到齐,开始...");
});

for (int i = 0; i < 7; i++) {
    new Thread(() -> {
        // 执行任务
        barrier.await(); // 等待其他线程
        System.out.println("继续执行");
    }).start();
}

应用场景:

  • 多线程分批处理
  • 阶段性任务

Semaphore 信号量 #

// 3个许可证
Semaphore semaphore = new Semaphore(3);

for (int i = 0; i < 10; i++) {
    new Thread(() -> {
        try {
            semaphore.acquire(); // 获取许可证
            // 执行受限资源访问
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release(); // 释放许可证
        }
    }).start();
}

应用场景:

  • 流量控制
  • 数据库连接池

线程问题 #

线程安全保证 #

机制 原理 开销
synchronized monitor锁
Lock AQS
volatile 内存屏障
Atomic CAS

死锁条件与避免 #

// 死锁示例
Object lock1 = new Object();
Object lock2 = new Object();

// 线程1
synchronized (lock1) {
    Thread.sleep(100);
    synchronized (lock2) {} // 等待lock2
}

// 线程2
synchronized (lock2) {
    Thread.sleep(100);
    synchronized (lock1) {} // 等待lock1
}

死锁四个条件:

  1. 互斥条件
  2. 请求与保持
  3. 不可剥夺
  4. 循环等待

避免方案:

  1. 固定加锁顺序
  2. 锁超时
  3. 死锁检测

线程顺序执行 #

// 方案1: join()
Thread A = new Thread(() -> System.out.println("A"));
Thread B = new Thread(() -> System.out.println("B"));
Thread C = new Thread(() -> System.out.println("C"));

A.start();
A.join();  // 等待A完成

B.start();
B.join();  // 等待B完成

C.start();

// 方案2: CountDownLatch
CountDownLatch latch1 = new CountDownLatch(1);
CountDownLatch latch2 = new CountDownLatch(1);

new Thread(() -> {
    System.out.println("A");
    latch1.countDown();
}).start();

new Thread(() -> {
    latch1.await();
    System.out.println("B");
    latch2.countDown();
}).start();

new Thread(() -> {
    latch2.await();
    System.out.println("C");
}).start();

// 方案3: 单线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println("A"));
executor.submit(() -> System.out.println("B"));
executor.submit(() -> System.out.println("C"));
executor.shutdown();

虚拟线程 Virtual Thread(JDK 21+) #

Project Loom 项目成果,JDK 21 正式转正,轻量级用户态线程,海量并发

什么是虚拟线程? #

平台线程(传统线程):

  • 由操作系统内核管理 1:1 映射
  • 栈内存默认 1MB,每个线程占用较大内存
  • 调度由操作系统完成,上下文切换代价大
  • 普通服务器几千个就到顶,无法百万并发

虚拟线程:

  • 由 JVM 管理 M:N 调度(M个虚拟线程映射到N个平台线程)
  • 栈初始仅几百字节,可动态扩容
  • 调度在用户态完成,上下文切换极快
  • 支持百万级并发,内存占用小

JVM 用户态

操作系统内核

平台线程 1

平台线程 2

平台线程 K

虚拟线程 1

虚拟线程 2

...

虚拟线程 100000

核心原理 #

1. 堆栈分割

虚拟线程的栈不连续:

  • 栈帧分布在 Java 堆上
  • 当虚拟线程阻塞时,JVM 将堆栈固定在堆
  • 释放平台线程去运行其他虚拟线程
  • 继续执行时恢复栈

2. 抢占式调度 vs 协作式调度

  • 虚拟线程是抢占式调度,不由用户自己 yield
  • JVM 自动调度,阻塞时自动让出平台线程
  • 用户代码不需要修改就能享受好处

3. 载体线程 Carrier Thread

平台线程就是虚拟线程的"载体",一个载体可以跑多个虚拟线程。虚拟线程阻塞 → 载体跑去跑其他虚拟线程 → 阻塞解除 → 再找个载体继续跑。

代码示例 #

创建虚拟线程:

// 1. 直接创建
Thread vt = Thread.ofVirtual().start(() -> {
    System.out.println("Hello virtual thread");
});

// 2. 虚拟线程线程池
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 0; i < 100000; i++) {
    executor.submit(() -> {
        // 处理请求,每个请求一个虚拟线程
        return result;
    });
}
executor.close();

// 3. Spring Boot 3.2+ 配置虚拟线程
spring:
  threads:
    virtual:
      enabled: true  # 开启后处理请求都是虚拟线程

每个请求一个虚拟线程

  • Web 服务器每个请求开一个虚拟线程
  • 即使十万并发,内存也扛得住
  • 编程模型还是同步阻塞,简单易懂
  • 不需要改造成反应式(Netty+WebFlux)就能获得高并发

虚拟线程 vs 平台线程 对比 #

对比项 平台线程(传统) 虚拟线程
映射 1:1 内核线程 M:N 用户态
栈大小 1MB 固定 几百B ~ 数MB 动态
上下文切换 内核态,慢 用户态,快
最大并发 几千 几十万~几百万
内存占用
适用场景 CPU密集,少量线程 IO密集,海量并发

最佳实践 #

1. 适用场景 ✅ #

  • Web 服务:每个请求一个线程,IO密集
  • RPC 调用:大量等待IO的并发请求
  • 批量处理:十万个任务并行处理
  • 消息消费:多消费者并行处理

2. 不适用场景 ❌ #

  • CPU 密集计算:没有阻塞,虚拟线程没优势
  • 非常长生命周期线程:比如后台定时任务,平台线程足够
  • synchronized 块内阻塞:JDK 21 之前虚拟线程在 synchronized 会 pinned 住平台线程,JDK 21+ 已经改进支持虚拟线程协作唤醒

3. 内存配置 #

虚拟线程大量创建,每个都有栈在堆:

  • 如果百万虚拟线程,注意堆内存大小 -Xmx 设置足够
  • 栈自动扩容收缩,不用太担心

4. 依赖第三方库注意事项 #

  • 如果第三方库使用 ThreadLocal,大量虚拟线程可能导致内存升高
  • 短期请求处理完虚拟线程死亡,ThreadLocal 会清理,问题不大
  • 长期池化虚拟线程需要注意内存泄漏

为什么虚拟线程好? #

传统平台线程 + 反应式(WebFlux)方案问题:

  • 反应式编程模型复杂,回调/ Mono/Flux 难写难调试
  • 对依赖库要求高,很多阻塞API不兼容
  • 开发效率低,学习曲线陡

虚拟线程方案:

  • 编程模型不变,还是同步阻塞写法
  • JVM 底层搞定并发,用户代码不用改
  • 同样获得百万并发高吞吐量
  • 开发效率高,调试简单

常见问题 #

Q: 虚拟线程会完全取代平台线程吗? A: 不会。CPU密集计算还是用平台线程更好,没有额外调度开销。虚拟线程解决IO密集海量并发问题。

Q: 虚拟线程更快吗? A: 单个请求不一定更快,但高并发下吞吐量高很多,能支持更多并发请求。

Q: 虚拟线程有没有什么坑? A:

  • 内存:百万虚拟线程需要更大堆
  • synchronized:旧JDK会pin住平台线程,JDK 21+修复
  • ThreadLocal:大量线程持有会增加内存使用

🎯 面试题汇总 #

基础 #

  1. 线程状态转换?

答:

Java线程在生命周期中共有6种状态(见java.lang.Thread.State枚举):

状态 说明 转换条件
NEW 新建状态 创建了Thread对象但还未调用start()
RUNNABLE 可运行状态 调用start()后,等待CPU调度
BLOCKED 阻塞状态 等待获取synchronized
WAITING 无限等待 wait()/join()/LockSupport.park()
TIMED_WAITING 计时等待 sleep(time)/wait(time)/parkNanos
TERMINATED 终止状态 线程执行完毕

转换流程:

new Thread()

start()

获得CPU

时间片用完

synchronized/wait

wait/join/park

sleep/wait/timedPark

竞争到锁

notify/notifyAll/unpark

超时/唤醒

执行完毕

New

Runnable

Running

Blocked

Waiting

TimedWaiting

Terminated

注意:RUNNABLE包含了正在运行可运行两种情况,操作系统层面区分,但Java层面合并为一个状态。

  1. sleep() 和 wait() 区别?

答:

区别点 sleep() wait()
所属类 Thread类的静态方法 Object类的实例方法
释放锁 ❌ 不释放锁 ✅ 释放锁,进入等待池
唤醒条件 时间到自动唤醒 需要notify()/notifyAll()唤醒
使用场景 暂停指定时间 线程间通信,等待条件
异常处理 需要捕获InterruptedException 需要捕获InterruptedException
位置 可以在任何地方调用 只能在synchronized块中调用

核心区别: sleep()是线程暂停,不释放锁;wait()是线程等待,释放锁让其他线程抢锁。

  1. start() 和 run() 区别?

答:

区别点 start() run()
作用 启动新线程,调用本地方法start0()注册线程到操作系统 线程的执行体,普通方法调用
线程创建 真正创建新线程,进入就绪队列 还是在当前线程执行,没有新线程
调用次数 一个线程只能调用一次 可以被多次调用
运行方式 异步执行,并发运行 同步执行,在当前线程直接运行

示例:

// 正确:启动新线程
new Thread(() -> System.out.println("hello")).start();

// 错误:还是在主线程执行,没有并发
new Thread(() -> System.out.println("hello")).run();

锁机制 #

  1. synchronized 和 Lock 区别?

答:

特性 synchronized ReentrantLock
实现方式 JVM底层实现,字节码monitorenter/monitorexit API层面实现,基于AQS队列同步器
锁类型 可重入 可重入
获取锁状态 无法判断是否获取到锁 可以通过tryLock()尝试获取锁,非阻塞
释放锁 自动释放(异常也会自动释放) 必须手动在finally中释放,否则会死锁
公平锁 只有非公平锁,无法设置 支持公平/非公平两种模式,构造器可指定
响应中断 不支持,一直等待 支持lockInterruptibly()可响应中断
条件变量 只有一个条件队列,wait/notify 支持多个Condition,可以精确唤醒
锁对象 可以修饰方法、代码块 只能是API对象

选择原则:

  • synchronized适合简单并发场景,易用不易错
  • ReentrantLock适合复杂场景:需要公平锁、超时获取、多个条件等
  1. synchronized 锁升级过程?

答:

为了减少锁的性能开销,JDK 1.6 对synchronized进行了优化,引入了锁升级过程:

单线程重复访问

CAS自旋竞争

挂起线程等待

无锁

偏向锁

轻量级锁

重量级锁

各阶段说明:

阶段 说明 适用场景 实现
偏向锁 Mark Word存储线程ID,只有一个线程进入时,不竞争就是偏向 单线程重复进入同步块 CAS修改Mark Word
轻量级锁 多个线程交替竞争,用CAS自旋尝试获取锁 低竞争场景,线程交替执行 在栈帧建锁记录,CAS替换Mark Word
重量级锁 CAS自旋多次失败后膨胀为重量级锁,依赖操作系统mutex 高竞争场景,多线程同时抢锁 内核态阻塞唤醒

⚠️ 重要特性:锁只能升级不能降级,所以一旦膨胀为重量级锁就无法回去。

  1. volatile 作用和原理?

答:

两大作用:

  1. 保证可见性:一个线程修改了共享变量的值,其他线程能立即看到最新值
  2. 禁止指令重排序:禁止编译器和CPU对指令进行重排序优化

底层原理:

  • 通过**内存屏障(Memory Barrier)**实现
  • 写volatile后插入store屏障,强制刷新到主存
  • 读volatile前插入load屏障,强制从主存读取
  • CPU层面使用LOCK前缀指令,触发缓存一致性协议(MESI),保证缓存行同步

特性限制:

  • 不保证原子性:复合操作(如count++)仍然需要锁
  • ✅ 适用场景:状态标记位、双重检查锁定(DCL)单例模式

示例:

// 正确:状态标记,停止线程
private volatile boolean running = true;

public void stop() {
    running = false; // 其他线程立即可见
}

public void run() {
    while (running) { // 每次都从主存读,能正确停止
        // do work
    }
}
  1. CAS 和 ABA 问题?

答:

CAS是什么?

  • CAS = Compare-And-Swap,比较并交换,是一种原子操作
  • 底层:CPU指令支持(cmpxchg指令),无锁并发的基础
  • 原理:V内存值,A预期值,B新值。如果V == AV = B,返回成功;否则失败

ABA问题:

  • 一个线程把值从A改成B,另一个线程又改回A
  • 此时CAS检查发现值还是A,误认为没有被修改过,实际上被改过
  • 这在某些业务场景会导致问题

解决方法:

  • 使用**版本号(邮票)**机制,每次修改都递增版本号
  • Java中提供了AtomicStampedReference类来解决

示例代码:

// 原始ABA问题
AtomicReference<String> ref = new AtomicReference<>("A");
ref.compareAndSet("A", "B"); // 成功
ref.compareAndSet("B", "A"); // 又改回A,CAS无法发现

// 解决:带版本号的原子引用
AtomicStampedReference<String> ref = 
    new AtomicStampedReference<>("A", 1);

int[] stampHolder = new int[1];
String currentValue = ref.get(stampHolder);
int currentStamp = stampHolder[0];

// 比较不仅比较值,还要比较版本号
ref.compareAndSet(currentValue, "B", 
                 currentStamp, currentStamp + 1);

线程池 #

  1. 线程池七大参数?

答:

ThreadPoolExecutor(
    corePoolSize,      // 1. 核心线程数
    maximumPoolSize,   // 2. 最大线程数
    keepAliveTime,     // 3. 空闲线程存活时间
    TimeUnit unit,     // 4. 时间单位
    workQueue,         // 5. 工作队列
    threadFactory,     // 6. 线程工厂
    handler            // 7. 拒绝策略
)

参数详解:

参数 说明
corePoolSize 核心线程数,核心线程创建后不会销毁,一直常驻
maximumPoolSize 线程池允许创建的最大线程数
keepAliveTime 非核心线程空闲超过这个时间就会被销毁回收
unit keepAliveTime的时间单位
workQueue 保存等待执行任务的阻塞队列
threadFactory 创建线程的工厂,可自定义线程名称方便排查
handler 队列满且线程数达到maximumPoolSize时的拒绝策略
  1. 工作流程?

答:

线程池的工作流程图:

提交任务

核心线程
是否已满?

创建核心线程执行任务

工作队列
是否已满?

任务入队列等待

最大线程数
是否已满?

创建非核心线程执行任务

执行拒绝策略

详细步骤:

  1. 提交任务,如果核心线程没满 → 创建核心线程执行

  2. 如果核心线程已满 → 尝试把任务放入工作队列排队

  3. 如果工作队列也满了 → 看最大线程数有没有到顶

  4. 如果最大线程数没到顶 → 创建非核心线程执行

  5. 如果最大线程数也到顶 → 触发拒绝策略

  6. 如何合理配置线程池?

答:

根据任务的CPU密集型还是IO密集型来配置:

任务类型 说明 配置公式 示例(8核CPU)
CPU密集型 大量计算、逻辑处理,很少阻塞 核心线程数 = CPU核心数 + 1 8 + 1 = 9
IO密集型 大量IO操作(网络、数据库),经常阻塞 核心线程数 = 2 × CPU核心数核心线程数 = CPU核心数 / (1 - 阻塞系数) 2 × 8 = 16

更多建议:

  • 队列使用有界队列:防止任务无限堆积导致OOM
  • 自定义线程工厂:给线程起有意义的名字,方便问题排查
  • 拒绝策略使用CallerRunsPolicy:让调用者线程自己执行,避免丢弃任务
  • 优雅关闭:使用shutdown() + awaitTermination(),最后shutdownNow()

最佳实践示例:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    Runtime.getRuntime().availableProcessors() * 2,  // IO密集型
    Runtime.getRuntime().availableProcessors() * 4,
    60L,
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000),  // 有界队列
    new ThreadFactoryBuilder().setNameFormat("biz-pool-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy()
);
  1. 为什么阿里规范禁止 Executors 创建线程池?

答:

Executors创建线程池存在资源耗尽风险

创建方式 问题 风险
FixedThreadPool / SingleThreadPool 使用LinkedBlockingQueue无界队列 任务可以无限堆积,最终导致OOM(OutOfMemoryError)
CachedThreadPool / ScheduledThreadPool 允许创建最大线程数为Integer.MAX_VALUE 可以无限创建线程,最终导致OOM

正确做法: 直接使用ThreadPoolExecutor构造器,手动指定参数:

  • 使用有界队列,控制队列容量
  • 限制最大线程数,防止线程爆炸
  • 根据业务类型合理设置参数

这样更容易理解线程池运行原理,也避免了隐藏的资源风险。

进阶 #

  1. ThreadLocal 原理及内存泄漏?

答:

原理:

  • ThreadLocal并不存储值,每个Thread内部有一个ThreadLocalMap
  • ThreadLocal作为key,值作为value存在这个Map里
  • 不同线程访问互相隔离,每个线程只能看到自己的值

核心结构:

Thread

ThreadLocalMap

Entry&[&] table

Entry

key = ThreadLocal

value = 实际值

内存泄漏问题:

  • Entry继承WeakReference<ThreadLocal>,key是弱引用
  • 如果ThreadLocal没有强引用,GC会回收key
  • 但value仍然存在强引用(来自Thread → ThreadLocalMap → Entry → value)
  • key被回收后变成null,但value无法访问,造成内存泄漏
  • 尤其是在线程池中,线程会复用长期存活,泄漏会累积

解决方法:

  • 用完ThreadLocal后,必须调用remove()删除Entry
  • 最佳实践:try-finally模式
try {
    threadLocal.set(value);
    // 业务逻辑
} finally {
    threadLocal.remove(); // 必须清理
}

JDK 8 之后,set()rehash()会主动清理key为null的entry,但不能完全依赖。

  1. 如何捕获线程异常?

答:

有几种方式:

方式1:try-catchrun()方法内部捕获

new Thread(() -> {
    try {
        // 业务代码
    } catch (Exception e) {
        // 处理异常
        log.error("线程异常", e);
    }
}).start();

方式2:设置UncaughtExceptionHandler

Thread thread = new Thread(() -> {
    throw new RuntimeException("异常");
});
thread.setUncaughtExceptionHandler((t, e) -> {
    log.error("线程{}未捕获异常: {}", t.getName(), e);
});
thread.start();

方式3:线程池通过Future捕获

ExecutorService executor = Executors.newFixedThreadPool(1);
Future<?> future = executor.submit(() -> {
    throw new RuntimeException("任务异常");
});
try {
    future.get(); // 这里会抛出ExecutionException包装异常
} catch (ExecutionException e) {
    Throwable cause = e.getCause(); // 获取原始异常
    log.error("任务异常", cause);
}

方式4:自定义线程工厂,统一设置异常处理器

ThreadFactory factory = r -> {
    Thread t = new Thread(r);
    t.setUncaughtExceptionHandler((thread, e) -> {
        log.error("线程异常: {}", e.getMessage());
    });
    return t;
};
  1. 如何保证线程顺序执行?

答:

常见有三种方案:

方案1:使用join()方法

Thread A = new Thread(() -> System.out.println("A"));
Thread B = new Thread(() -> System.out.println("B"));
Thread C = new Thread(() -> System.out.println("C"));

A.start();
A.join();  // 等待A完成再继续

B.start();
B.join();  // 等待B完成再继续

C.start(); // A → B → C 顺序执行

方案2:使用CountDownLatch计数器

CountDownLatch latch1 = new CountDownLatch(1);
CountDownLatch latch2 = new CountDownLatch(1);

new Thread(() -> {
    System.out.println("A");
    latch1.countDown();
}).start();

new Thread(() -> {
    latch1.await(); // 等待A完成
    System.out.println("B");
    latch2.countDown();
}).start();

new Thread(() -> {
    latch2.await(); // 等待B完成
    System.out.println("C");
}).start();

方案3:使用单线程线程池

ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println("A"));
executor.submit(() -> System.out.println("B"));
executor.submit(() -> System.out.println("C"));
executor.shutdown();

单线程池保证任务按提交顺序排队执行。

方案4:使用CompletableFuture链式调用(Java 8+)

CompletableFuture.runAsync(() -> System.out.println("A"))
    .thenRun(() -> System.out.println("B"))
    .thenRun(() -> System.out.println("C"));
  1. 什么是虚拟线程?和平台线程区别?

答:

虚拟线程是JDK 21引入的轻量级线程,来自Project Loom项目,由JVM在用户态管理,不直接映射操作系统线程。

对比:

对比项 平台线程(传统线程) 虚拟线程
映射关系 1:1 映射操作系统内核线程 M:N 映射,M个虚拟线程映射N个平台线程
栈大小 默认1MB固定大小,大块分配 初始几百字节,堆上动态扩容
调度 操作系统内核调度 JVM用户态调度,阻塞时自动卸载
上下文切换 内核态切换,代价高 用户态切换,代价极低
并发数量 几千个就到顶,受内存限制 支持几十万~几百万并发
内存占用 高,每个1MB 低,动态分配

核心原理:

  • 虚拟线程的栈放在Java堆上,不占用内核栈
  • 当虚拟线程阻塞时,JVM将其栈快照固定在堆,释放载体平台线程
  • 阻塞解除后,再找一个空闲平台线程继续执行
  • 对用户代码透明,编程模型不变,还是同步阻塞写法
  1. 虚拟线程适用场景?最佳实践?

答:

✅ 适用场景:

  • Web服务:每个请求一个虚拟线程,IO密集型场景,支持十万并发
  • RPC/网络调用:大量等待IO的并发请求
  • 批量处理:十万个任务并行处理
  • 消息消费:多消费者并行处理队列消息

❌ 不适用场景:

  • CPU密集计算:没有阻塞,虚拟线程调度没有优势,反而增加开销
  • 非常长生命周期线程:后台定时任务等,平台线程足够

最佳实践:

  1. 每个任务一个虚拟线程

    // Web服务:每个请求一个虚拟线程
    ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
    for (int i = 0; i < 100000; i++) {
        executor.submit(() -> handleRequest());
    }
    
  2. Spring Boot 3.2+ 直接开启配置

    spring:
      threads:
        virtual:
          enabled: true
    
  3. 注意事项:

    • ThreadLocal:大量虚拟线程使用ThreadLocal会增加内存使用,短期任务问题不大
    • synchronized:JDK 21之前,虚拟线程在synchronized块阻塞会pin住平台线程,JDK 21已修复
    • 内存:百万虚拟线程需要更大堆内存 -Xmx 设置足够

优势总结:

  • 保持同步阻塞编程模型,简单易懂,调试方便
  • 不需要改造成复杂的反应式编程就能获得高吞吐量
  • 开发效率高,内存效率好

🔗 相关笔记 #


最后更新: 2026-04-28