Java并发核心面试知识点

Java并发核心面试知识点


前言

本文为自用复习笔记,核心用于梳理Java并发编程核心高频考点,方便后续快速回顾、巩固底层细节,规避面试易踩坑点,查漏补缺遗忘的基础原理。

本次笔记将围绕线程生命周期与start/run区别、线程池7大参数+工作流程+拒绝策略、synchronized锁升级全流程、volatile两大核心特性、ThreadLocal原理与内存泄漏五大面试必考模块展开,同步对比JDK8与最新JDK21的差异,贴合后端开发、Java全栈、架构岗面试标准。

本文结合源码深度拆解、面试高频设问、落地避坑技巧整理而成,和HashMap笔记逻辑保持一致,方便成套复习。

PS:

最近跳槽准备Java全栈架构岗,并发是面试必问模块,之前业务写多了,底层源码细节、版本差异、面试坑点很容易遗忘,专门整理成逐句可背诵的复习笔记,把原理、流程、易错点一次性捋清楚,面试不慌。


一、Thread 线程核心:生命周期、start()与run()区别

本模块为多线程基础开篇题,面试100%会考,核心区分API用法、底层逻辑、JDK版本差异,杜绝死记硬背。

1.1 start() 与 run() 核心区别(面试必问第一题)

废话少说,先上Thread类核心源码,对应JDK8原生实现:

java 复制代码
// 线程启动核心方法,native修饰,JVM底层实现
public synchronized void start() {
    // 线程状态校验,只能启动一次,否则抛出IllegalThreadStateException
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    // 线程加入线程组
    group.add(this);
    boolean started = false;
    try {
        // 本地方法,真正创建操作系统线程,启动线程
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
        }
    }
}

// 业务逻辑执行方法,Runnable接口重写方法
@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

// native本地方法,JVM底层实现,与操作系统交互创建线程
private native void start0();
1.1.1 核心执行流程与本质区别

start() 方法:真正的线程启动方法

  1. 校验线程状态,一个线程只能调用一次start(),重复调用直接抛异常,这是面试高频坑点;

  2. 通过native本地方法start0(),向操作系统申请创建内核线程,完成线程初始化,将线程转为就绪状态

  3. 等待操作系统CPU时间片调度,调度成功后,自动回调run()方法执行业务逻辑;

  4. 核心作用:开启新的独立执行线程,实现多线程并发执行

run() 方法:普通的业务逻辑方法

  1. 就是Runnable接口的普通重写方法,没有任何特殊语法修饰,就是普通类的成员方法;

  2. 直接调用run()方法,不会创建新线程,只会在当前主线程中同步执行run()里面的代码,完全没有多线程效果;

  3. 核心作用:封装线程需要执行的业务逻辑,只能被start()启动后,由JVM自动回调执行。

1.1.2 极简对比表格(面试直接背诵)
对比维度 start() 方法 run() 方法
底层修饰 synchronized 修饰 + native本地方法 普通接口重写方法,无特殊修饰
线程创建 会创建新的操作系统线程 不会创建任何线程,复用当前线程
调用限制 单个线程只能调用1次,重复调用抛异常 可以无限次重复调用,无任何限制
执行效果 多线程并发执行,异步逻辑 同步串行执行,和普通方法无区别
核心作用 启动线程,触发线程生命周期 封装线程业务执行逻辑
1.1.3 面试易错点
  1. 启动线程必须用start(),直接调run()=白写,没有多线程效果;

  2. 线程启动后进入终止状态,无法再次调用start()重启,只能新建线程对象。

1.2 线程完整生命周期:JDK8 vs JDK21

线程生命周期由Thread类内部枚举State定义,JDK8为官方标准6状态,JDK21引入虚拟线程后,状态语义、底层实现有优化,面试必须区分清楚。

1.2.1 JDK8 标准6种线程状态(必考,全版本通用基础)

源码枚举定义:

java 复制代码
public enum State {
    NEW,                // 新建状态
    RUNNABLE,           // 可运行状态
    BLOCKED,            // 阻塞状态
    WAITING,            // 无限等待状态
    TIMED_WAITING,      // 限时等待状态
    TERMINATED;         // 终止状态
}
  1. NEW(新建状态):线程对象被创建,还没有调用start()方法,仅存在Java堆内存中,未和操作系统线程关联;

  2. RUNNABLE(可运行状态):调用start()之后进入该状态,包含「就绪」和「运行中」两个子状态,就绪等待CPU调度,调度后运行,是Java线程唯一的"执行中"状态;

  3. BLOCKED(阻塞状态) :线程等待synchronized独占锁,进入同步代码块/方法时,锁被其他线程占用,就会进入该状态,仅针对synchronized锁

  4. WAITING(无限等待状态) :线程主动进入无限等待,必须被其他线程唤醒才能恢复,不会自动苏醒,触发方法:Object.wait()Thread.join()LockSupport.park()

  5. TIMED_WAITING(限时等待状态) :线程主动进入带超时时间的等待,超时时间到自动苏醒,无需其他线程唤醒,触发方法:Thread.sleep(long)Object.wait(long)Thread.join(long)

  6. TERMINATED(终止状态) :线程run()方法执行完毕、异常终止未捕获,线程生命周期结束,终止后无法恢复,不能再次启动

1.2.2 JDK21 虚拟线程优化差异
  1. 线程状态枚举名称、数量完全不变,兼容JDK8语法,面试基础答案不变;

  2. 虚拟线程无独立操作系统内核线程,RUNNABLE状态下,阻塞、等待操作不会阻塞平台线程,CPU利用率大幅提升;

  3. 虚拟线程的BLOCKED、WAITING状态为纯用户态管控,不再和操作系统线程挂钩,生命周期开销极低。

1.2.3 面试核心考点

线程只有6种状态,RUNNABLE包含运行和就绪,没有专门的"运行状态";BLOCKED仅针对synchronized锁,Lock锁的等待是WAITING状态,二者严格区分。


二、ThreadPoolExecutor 线程池:7大参数、工作流程、拒绝策略

线程池是Java并发面试TOP1考点,和HashMap put流程、扩容机制同等重要,必须把参数含义、执行步骤、拒绝策略、设计逻辑背到脱口而出。

2.1 线程池7大核心参数(全拆解,一个都不能漏)

JDK8线程池核心实现类为ThreadPoolExecutor,完整构造方法源码:

java 复制代码
public ThreadPoolExecutor(
    int corePoolSize,               // 核心线程数
    int maximumPoolSize,            // 最大线程数
    long keepAliveTime,             // 非核心线程空闲存活时间
    TimeUnit unit,                  // 时间单位
    BlockingQueue<Runnable> workQueue, // 阻塞任务队列
    ThreadFactory threadFactory,    // 线程工厂
    RejectedExecutionHandler handler // 拒绝策略处理器
) {
    // 源码校验逻辑省略
}

7个参数逐个拆解,附带面试设问点、默认值、设计逻辑:

  1. corePoolSize 核心线程数线程池长期存活的核心线程数量,默认情况下,核心线程会一直存活,即使空闲也不会被回收,是线程池的基础承载能力,建议设置为CPU核心数。

  2. maximumPoolSize 最大线程数线程池能创建的最大线程总数,包含核心线程+非核心临时线程,是线程池的承载上限,当任务爆发时,最多能创建这么多线程处理任务。

  3. keepAliveTime 非核心线程空闲存活时间非核心临时线程,空闲时间超过该阈值,就会被线程池回收销毁,释放资源,核心线程默认不受该参数控制。

  4. unit 时间单位存活时间的单位,常用秒、毫秒,配合keepAliveTime使用。

  5. workQueue 阻塞任务队列 核心线程全忙时,新增任务会进入该队列排队等待执行,是线程池的缓冲核心,常用队列:ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列,慎用)、SynchronousQueue(不存储元素的同步队列)。

  6. threadFactory 线程工厂用于创建新线程的工厂,默认使用Executors默认工厂,建议自定义线程工厂,设置线程名称、优先级、守护线程属性,方便问题排查。

  7. handler 拒绝策略线程池达到最大线程数、阻塞队列已满,无法接收新任务时,触发拒绝策略,处理新增任务,JDK内置4种标准实现。

2.2 线程池完整工作流程(分步拆解,面试必背)

完全对标HashMap put方法流程,按执行顺序分步拆解,一步都不能错:

  1. 任务提交,判断核心线程数 调用execute()/submit()提交任务,首先判断当前运行的线程数,是否小于核心线程数corePoolSize;如果小于,直接创建新的核心线程执行任务,即使有核心线程空闲,不会复用空闲线程。

  2. 核心线程已满,进入阻塞队列排队 如果当前线程数≥核心线程数,判断阻塞队列workQueue是否已满;如果未满,将新任务加入阻塞队列排队等待,等待空闲核心线程拉取任务执行。

  3. 队列已满,创建非核心临时线程 如果阻塞队列已满,判断当前线程总数,是否小于最大线程数maximumPoolSize;如果小于,立即创建非核心临时线程执行任务,快速处理爆发任务。

  4. 达到最大线程数,触发拒绝策略 如果当前线程总数已经达到最大线程数,队列也已满,线程池完全无法接收新任务,执行预设的拒绝策略,处理新增任务

  5. 任务执行完毕,线程回收逻辑任务执行完成后,线程会循环从阻塞队列拉取任务执行;非核心线程空闲时间超过keepAliveTime,就会被回收,核心线程默认一直存活。

面试易错点

核心线程未满时,不会复用空闲线程,一定会新建核心线程;无界队列会导致最大线程数、拒绝策略完全失效,生产环境绝对禁止使用无界队列。

2.3 JDK内置4种拒绝策略

全部实现RejectedExecutionHandler接口,逐个拆解含义、适用场景、面试考点:

  1. **AbortPolicy(默认拒绝策略)**直接抛出未检查异常RejectedExecutionException,阻止任务提交,不丢弃任务,让开发者感知到任务被拒绝,生产环境常用,方便告警排查。

  2. CallerRunsPolicy(调用者执行策略)不抛弃任务,不抛出异常,将任务回退给提交任务的主线程同步执行,降低主线程提交任务的速率,实现隐式限流,不会丢失任务,适合低并发、不允许任务丢失的场景。

  3. **DiscardPolicy(静默丢弃策略)**直接静默丢弃当前无法处理的新任务,不抛出任何异常,不做任何处理,完全无感知,生产环境绝对禁止使用,会导致任务无声丢失。

  4. **DiscardOldestPolicy(丢弃最老任务策略)**丢弃阻塞队列中,排队时间最长的老任务,腾出队列空间,将当前新任务加入队列,尝试提交执行,会丢失历史排队任务,慎用。

面试核心:默认策略是AbortPolicy,生产环境优先用默认策略+自定义告警,其次用CallerRunsPolicy,禁止用静默丢弃策略。


三、synchronized 锁:完整锁升级全流程

synchronized是Java内置独占锁,JDK1.6之前是重量级锁,性能极差;JDK1.6之后引入锁升级机制,大幅优化性能,是面试必考的底层原理题,全程无锁→偏向锁→轻量级锁→重量级锁,一步都不能错。

3.1 锁升级核心前提

  1. synchronized锁对象,是Java对象头中的Mark Word,锁状态、线程ID、指针全部存在对象头里,锁升级本质就是Mark Word里的标记位修改;

  2. 锁升级只能单向升级,不能降级,只会从无锁逐步升级为重量级锁,不会逆向回退,只有在全局安全点(STW)才会做锁降级优化;

  3. 锁升级的核心设计逻辑:无竞争用最轻的锁,竞争逐步升级,用最低的开销实现线程同步,用空间换时间,减少内核态切换开销。

3.2 完整锁升级流程(分步拆解,底层原理)

阶段1:无锁状态

锁对象没有任何线程竞争,对象头Mark Word存储对象的哈希码、分代年龄,没有任何锁标记,线程可以随意访问,无任何同步开销。

阶段2:偏向锁(默认开启,JDK15默认关闭,JDK21已废弃)

适用场景:只有一个线程反复获取锁,完全无多线程竞争,是最轻量级的锁,几乎无同步开销。

  1. 第一个线程访问同步代码,获取锁时,锁对象Mark Word改为偏向模式,CAS操作将当前线程ID写入对象头;

  2. 后续该线程再次获取锁,无需任何CAS操作、无需同步判断,只需要校验对象头里的线程ID是自己,就可以直接获取锁,开销极低;

  3. 一旦有第二个线程来竞争锁,偏向锁立即撤销,停止偏向模式,升级为轻量级锁。

阶段3:轻量级锁(自旋锁)

适用场景:多线程交替获取锁,锁持有时间极短,无长时间锁竞争,线程空循环自旋等待锁,避免进入内核态阻塞。

  1. 偏向锁撤销后,升级为轻量级锁,当前线程在自己的栈帧中创建锁记录(Lock Record)

  2. 通过CAS操作,将锁对象的Mark Word,替换为指向当前线程栈帧中锁记录的指针;

  3. CAS替换成功,当前线程成功获取轻量级锁,执行同步代码;

  4. CAS替换失败,说明有其他线程持有锁,当前线程自适应自旋等待,循环尝试CAS获取锁,不进入阻塞;

  5. 自旋一定次数仍未获取到锁,轻量级锁升级为重量级锁。

阶段4:重量级锁

适用场景:多线程激烈竞争锁,锁持有时间长,自旋等待浪费CPU,是JDK1.6之前的原生锁,底层依赖操作系统互斥量Mutex实现。

  1. 轻量级锁自旋失败,升级为重量级锁,锁对象Mark Word指向操作系统互斥量指针;

  2. 未获取到锁的线程,全部进入操作系统阻塞队列,线程状态变为BLOCKED,释放CPU资源,不会空自旋浪费性能;

  3. 持有锁的线程释放锁后,操作系统会唤醒阻塞队列中的线程,重新竞争锁;

  4. 核心缺点:线程阻塞/唤醒需要用户态和内核态切换,开销极大,性能最低。

3.3 面试核心考点与易错点

  1. 锁升级顺序固定:无锁 → 偏向锁 → 轻量级锁 → 重量级锁,单向不可逆;

  2. JDK21已经完全废弃偏向锁,默认直接启动轻量级锁,面试回答基础流程不变,补充版本差异即可;

  3. synchronized是可重入锁,同一个线程多次获取锁,不会自己阻塞自己;

  4. 锁升级的核心目的:减少重量级锁的使用,降低线程上下文切换的开销。

  5. volatile修饰的共享变量,线程修改后,会立即强制刷新到主内存,同时失效其他所有线程工作内存中的变量副本;

  6. 其他线程读取该变量时,必须重新从主内存拉取最新值,保证所有线程总能读到变量的最新值,彻底解决可见性问题;

  7. 底层实现:缓存一致性协议(MESI),CPU层面实现缓存同步。

特性2:禁止CPU指令重排序

指令重排:CPU和编译器为了提升执行效率,会在不改变单线程执行结果的前提下,调整代码指令的执行顺序,单线程下无问题,多线程高并发下会导致逻辑异常。

  1. volatile修饰的变量,会在指令序列中插入内存屏障,严格限制指令的执行顺序,禁止屏障前后的指令重排;

  2. 写屏障:volatile变量写操作之前的所有指令,必须全部执行完毕,才能执行写操作,写操作之后立即刷新到主内存;

  3. 读屏障:volatile变量读操作之后的所有指令,必须等读操作执行完毕,从主内存拉取最新值后,才能执行;

  4. 经典应用场景:单例模式双重校验锁(DCL),必须用volatile修饰实例对象,禁止指令重排,避免线程获取到未初始化完成的半初始化对象。

4.2 面试必考三大核心误区(必背)

  1. volatile 绝对不保证原子性volatile只保证单次读、单次写的原子性,不保证复合操作的原子性。比如i++操作,分为「读取i值→计算+1→写入i值」三步,volatile无法保证三步操作的原子性,并发下依然会出现线程安全问题,解决原子性必须用synchronized、Lock或Atomic原子类。

  2. volatile 不能替代锁volatile只能解决可见性、有序性问题,无法解决原子性问题,而线程安全的三大要素是:原子性、可见性、有序性,三者缺一不可,volatile无法实现完整的线程同步。

  3. volatile 修饰的变量,每次读写都必须操作主内存不能使用线程本地缓存,所以volatile变量的读写性能,比普通变量略低,不要滥用volatile,只有需要保证多线程可见性、禁止重排的场景才使用。

4.3 最佳使用场景

  • 状态标记量:多线程控制的开关变量,比如线程停止标记、初始化完成标记;

  • 双重校验锁单例模式,禁止指令重排;

  • 无需原子性,只需要保证多线程可见性的共享变量。


五、ThreadLocal 核心原理与内存泄漏解决方案

ThreadLocal是面试高频压轴题,核心考察底层存储结构、核心原理、内存泄漏的完整根源、解决方案,很多人只背表面答案,底层逻辑一问就崩。

5.1 ThreadLocal 核心底层原理

先打破误区:ThreadLocal不是存储数据的容器,数据根本不存放在ThreadLocal中,这是90%的人面试答错的第一个点。

核心源码结构:

  1. 每个Thread线程对象内部,都有一个成员变量ThreadLocal.ThreadLocalMap threadLocals,这才是真正存储数据的容器;

  2. ThreadLocalMap是ThreadLocal的静态内部类,底层是Entry数组,以ThreadLocal对象为key,存储的业务数据为value;

  3. ThreadLocal的set/get/remove方法,本质都是操作当前线程自身的ThreadLocalMap,不同线程的Map相互隔离,互不干扰,天然实现线程封闭,不存在线程安全问题。

核心作用:实现线程内数据全局共享,线程间数据完全隔离,同一个线程内,任意代码位置都能通过ThreadLocal获取到存储的数据,无需层层传参。

5.2 ThreadLocal 内存泄漏的完整根源

前提:ThreadLocalMap的Entry结构设计
java 复制代码
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        // key为ThreadLocal对象,存入弱引用
        super(k);
        // value为强引用
        value = v;
    }
}

内存泄漏的完整链路,一步都不能错:

  1. Entry的key(ThreadLocal对象)是弱引用 ,value是强引用

  2. GC回收时,只要ThreadLocal对象没有外部强引用,就会被GC回收,Entry的key变为null;

  3. 只要当前线程还存活(比如线程池中的核心线程,永久存活),线程对象就一直强引用ThreadLocalMap,Map就一直强引用Entry,Entry就一直强引用value;

  4. key已经为null,这个value对象永远无法被访问、无法被GC回收,一直占用堆内存,造成内存泄漏

  5. 长时间运行,大量key为null的Entry堆积,最终会导致OOM内存溢出。

面试核心:内存泄漏的根本原因是key为null的Entry,value强引用无法释放,线程长期存活导致value无法GC回收,不是弱引用的问题,弱引用是为了减少内存泄漏,而非导致内存泄漏。

5.3 内存泄漏解决方案与最佳实践

  1. 强制规范:每次使用完ThreadLocal,必须手动调用remove()方法这是根治内存泄漏的唯一方案,remove()方法会主动删除当前线程Map中对应的Entry,清空key和value,断开强引用,GC可以正常回收,杜绝内存泄漏。

  2. 杜绝使用static修饰的ThreadLocal对象

    static修饰的ThreadLocal生命周期和类加载器绑定,永远不会被GC回收,key永远不会为null,会导致Entry和value永久强引用,加剧内存泄漏。

  3. 线程池场景下,必须严格清理ThreadLocal

    线程池线程会复用,上一个任务存储的数据,会残留在ThreadLocalMap中,导致下一个任务读到脏数据,同时引发内存泄漏,任务执行完毕必须手动remove。

面试核心考点总结

  1. 数据存在当前线程的ThreadLocalMap中,不是ThreadLocal本身;

  2. 内存泄漏根源:线程存活+key为null+value强引用无法释放;

  3. 根治方案:使用完毕手动调用remove()方法,没有其他替代品。


结尾PS

本文和HashMap面试笔记为成套复习资料,覆盖Java后端面试80%的高频考点,所有知识点均贴合源码、面试设问逻辑,可直接逐段背诵。复习时优先背流程、核心结论、易错点,底层原理辅助理解,面试就能稳定输出。

(注:文档部分内容可能由 AI 生成)

相关推荐
Java成神之路-2 小时前
Java SPI vs Spring SPI
java·spring
.NET修仙日记2 小时前
2026 .NET 面试八股文:高频题 + 答案 + 原理(高级核心篇)
面试·职场和发展·c#·.net·.net core·.net 8
希望永不加班3 小时前
Java数据类型陷阱:int和Integer的7个关键区别
java·开发语言
boonya3 小时前
Idea CC GUI插件如何通过 CC Switch 工具将 Claude Code 的后端配置为 DeepSeek 的 v4-pro 模型?
java·ide·intellij-idea
哈里谢顿3 小时前
redis的分布式设计
后端·面试
Fuly10243 小时前
技术经理面试相关--管理和沟通篇
面试·职场和发展
花千树-0103 小时前
从业务接口到 MCP Tool:多语言工程化实践指南(Python / TypeScript / Java)
java·python·rpc·typescript·api·mcp
qcx233 小时前
深度解析Deepseek V4:1M 上下文不是军备竞赛,是养 Agent 的人才知道的痛
java·开发语言
小则又沐风a3 小时前
基础的开发工具(2)---Linux
java·linux·前端