Java并发编程知识体系全梳理:从线程基础到源码级原理

发布时间:2026/9/9 1:59:34
Java并发编程知识体系全梳理:从线程基础到源码级原理 得先说明一点我写这篇东西不是让你背八股文而是从实际工作和面试两个维度把Java并发编程这条线彻底捋清楚。很多人在并发编程上卡壳不是因为知识点多而是因为知识点之间是散的学一个忘一个。你跟着这篇文章的节奏走一遍你会发现并发编程其实就那几件事多线程怎么管理共享数据怎么保护资源怎么协调性能怎么优化。把这四件事搞明白面试官问什么都绕不开你的手掌心。这篇文章针对的读者很明确已经写过Java代码、但对并发只有模糊概念的初中级开发者以及准备面试需要系统梳理并发知识体系的人。我会从入门基础讲到源码级原理每一块都配场景、配代码、配面试回答思路保证你能直接拿去用。1. 并发编程为什么是Java进阶的“分水岭”1.1 并发与并行的本质区别先统一一个概念很多人把并发和并行混着说面试一开口就露馅。并发是同一时间段内多个任务交替执行宏观上看起来像同时进行并行是同一时刻多个任务真的同时执行。单核CPU上只能实现并发多核CPU上才可能并行。Java的线程模型是“映射到操作系统原生线程”所以多线程程序是否真正并行取决于宿主机有多少个CPU核心。这个区别为什么重要因为面试官想听的核心是你知不知道并发编程解决的是什么问题。并发解决的是“资源利用率”和“响应速度”的平衡问题并行解决的是“计算吞吐量”的问题。你什么时候用多线程、用多少线程其实都围绕这两个目标来谈。1.2 并发编程要解决的三大核心问题并发编程容易出问题归根结底是三个维度出了问题原子性、可见性、有序性。原子性一段代码要么全部执行完要么不执行不能被线程调度打断。典型例子是i看起来是一行代码但底层是“读-改-写”三步操作两个线程同时执行就会互相覆盖。可见性一个线程修改了共享变量的值其他线程能不能立刻看到。这个问题是CPU缓存和指令重排导致的——线程A把变量写入了CPU缓存还没刷回主内存线程B读到的是旧值。有序性代码写出来的顺序不一定是CPU实际执行的顺序。编译器和CPU为了优化性能会指令重排单线程内不影响结果但多线程环境下可能导致诡异的问题。这三个问题必须背熟因为后面的锁、volatile、原子类、AQS统统都是为了解决它们。1.3 从JMMJava内存模型开始理解一切面试中聊到并发JMM是绕不开的地基。JMM规定所有变量存于主内存每个线程有自己的工作内存抽象模型对应CPU缓存。线程对变量的所有操作必须在工作内存中进行不能直接操作主内存而且不同线程之间无法直接访问对方的工作内存只能通过主内存中转。这个模型解释了为什么会出现可见性问题。你写个多线程程序一个线程改了变量值另一个线程读到的还是旧值就是因为修改还留在工作内存里没有同步回主内存。JMM还提供了一套规则来保证并发安全核心是happens-before原则。你不需要背全部规则但要记住几条最常用的程序次序规则一个线程内写在前面的代码 happens-before 写在后面的。锁规则解锁 happens-before 后续的加锁。volatile规则对volatile变量的写happens-before 后续的读。传递性A happens-before BB happens-before C则 A happens-before C。这几条背下来面试时表达“我知道为什么加锁能保证安全”逻辑就通了。2. 多线程基础线程创建、状态流转与关键方法2.1 创建线程的三种方式面试对比怎么答Java里创建线程有直接继承Thread、实现Runnable、实现Callable三种方式。直接继承Thread简单但它把任务和线程耦合在一起还受限于Java单继承不推荐。实现Runnable更灵活把任务和线程分离。实现Callable能拿到返回值配合FutureTask使用。但面试如果只答到这里只能算及格。我会补一句这些方法本质没有优劣都是把任务提交给线程执行。现在生产环境里更推荐直接用线程池把线程的创建和管理交给框架避免手动创建线程带来的资源开销。这样回答说明你有实际项目经验。2.2 线程生命周期与关键状态流转Java线程有六种状态面试必考NEW新建、RUNNABLE可运行、BLOCKED阻塞、WAITING等待、TIMED_WAITING超时等待、TERMINATED终止。这里要说明一个容易误解的点Thread.sleep()让线程进入的是TIMED_WAITING不释放锁Object.wait()让线程进入WAITING或TIMED_WAITING会释放锁。这是面试特别爱挖的陷阱。比如问“sleep和wait有什么区别”要答两点一个是释放锁的区别一个是指定等待时间的区别。sleep是Thread的静态方法wait是Object的方法。线程的经典状态流转路径要画得出来从创建到运行、阻塞、等待、终止每一步是由哪个方法触发的。我面试别人时只要问一个“线程被notify之后是直接运行吗”很多人就懵了。正确答案是notify之后线程进入BLOCKED状态要重新竞争锁抢到锁才回到RUNNABLE。这个问题能考察对状态机理解得是否深刻。2.3 volatile关键字面试必答的两个语义和一个局限volatile可能是并发编程里解释成本最低又最高频的考题。它有两个语义保证可见性、禁止指令重排。它不保证原子性。可见性靠的是内存屏障。写volatile变量时JVM会插入StoreStore和StoreLoad屏障强制把工作内存的修改刷回主内存读volatile变量时插入LoadLoad和LoadStore屏障强制从主内存读取最新值。禁止指令重排体现在单例模式的双重检查锁中。经典的DCL单例如果不加volatile会因为指令重排导致另一个线程拿到一个“半初始化”的对象。因为创建对象在字节码层面有四步分配内存、初始化字段、设置指向引用、将引用赋值给变量。步骤2和3可能被重排先赋值引用后初始化对象另一个线程拿到引用的瞬间对象还没构造完。volatile适合的场景有两个状态标记位的开关、单例模式的DCL实现。它不适合做计数器因为多线程并发读改写场景没有原子性。3. 同步机制全解析synchronized与Lock的底层博弈3.1 synchronized的三种用法与锁升级过程synchronized是Java内置锁用法三种修饰实例方法锁当前实例、修饰静态方法锁Class对象、修饰代码块锁指定对象。面试里经常问的一个细节是修饰静态方法和修饰实例方法不是同一把锁因为锁对象不同一个是Class一个是实例。JDK 1.6之后的synchronized做了大量优化引入锁升级机制无锁 → 偏向锁 → 轻量级锁 → 重量级锁。这个过程要理解本质偏向锁只有一个线程访问同步块时在对象头标记线程ID之后该线程再次进入不需要CAS。轻量级锁有其他线程竞争时升级为轻量级锁通过CAS获取锁自旋等待。重量级锁自旋超过阈值或竞争激烈升级为重量级锁底层依赖操作系统互斥量线程会被阻塞。面试如果被问到“synchronized是公平锁还是非公平锁”答案是非公平锁因为新线程可以插队获取锁不会被阻塞队列阻塞。3.2 ReentrantLock公平锁、可中断、条件变量的价值很多Java开发者面试时说“synchronized够用了没有用过Lock”这句话不是加分项。ReentrantLock比synchronized多了几个关键能力支持可重入synchronized也支持支持公平和非公平锁切换构造方法传true就是公平锁支持锁中断响应lockInterruptibly()支持超时获取锁tryLock(timeout, unit)还支持多个条件变量。synchronized使用wait/notify只有一个条件队列ReentrantLock通过newCondition()可以创建多个条件队列。典型场景是生产者消费者模型——用两个Condition分别管理“队列不满”和“队列不空”两个等待条件这样唤醒时更精确不会出现“生产者醒来发现队列还是满的”这种无效竞争。3.3 锁对比表格面试别只背一堆特性要会用维度synchronizedReentrantLock锁获取方式隐式获取/释放显式lock/unlock锁特性非公平锁公平/非公平可选中断响应不支持支持超时获取不支持支持条件对象单个wait/notify多个Condition底层实现字节码级monitor基于AQS性能JDK 1.6后差异不大差异不大真正到项目里怎么选?我建议对于简单的同步需求synchronized更简洁、不容易出错对于需要超时控制、条件变量、公平锁等高级功能的场景选ReentrantLock。我的经验是大多数时候synchronized配合并发容器就够了。3.4 死锁的产生条件与排查套路面试被问死锁是标配。产生死锁的四个必要条件互斥、持有并等待、不可剥夺、循环等待。理论上打破任意一个条件就能预防死锁但实际代码里最容易调整的是“资源获取顺序”。比如两个线程分别持有锁A和锁B然后都想获取对方的锁就会死锁。解决办法是让两个线程都按“先取A再取B”的顺序加锁从源头保证不会出现循环等待。排查死锁的真正技能是会用命令工具。我推荐两个jps找出Java进程PIDjstack pid导出线程dump文件中会有“Found one Java-level deadlock”这样的提示直接告诉你是哪两个线程互相等待。还有一种情况是死锁不明显线程堆栈里多个线程都停在BLOCKED状态可以根据线程调用栈往上追溯锁持有关系。4. 深入并发底层CAS与AQS的设计之美4.1 CAS原理与ABA问题CAS全称Compare-And-Swap比较并交换。它是一条CPU原子指令有三个操作数内存位置V、预期原值A、新值B。当且仅当V的值等于A时才把V更新为B否则不更新。整个过程是原子的。Java里AtomicInteger、LongAdder、ConcurrentHashMap的很多操作底层都是CAS。CAS的好处是不需要锁没有上下文切换性能高坏处是存在ABA问题和自旋开销。ABA问题线程1读到A线程2把A改成B又把B改回A线程1再次CAS时发现还是A以为数据没变过。解决方法是加版本号比如AtomicStampedReference。面试时别只回答“用版本号解决”要说清为什么ABA在有些场景危险比如链表结构中某个节点被回收再重建即使地址相同内容不同CAS就会把错误的节点链接上去。4.2 AQSJUC的基石源码级解读AQSAbstractQueuedSynchronizer是Java并发包的基础框架ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock都基于它实现。AQS核心就三样东西一个volatile int state状态位一个FIFO双向等待队列CLH队列变种以及基于模板方法模式设计的获取/释放逻辑。state通常用于表示锁的持有数量可重入锁里是重入次数信号量里是剩余许可数。竞争失败的线程会被封装成Node节点通过CAS插入等待队列尾部然后LockSupport.park()挂起。释放锁时唤醒队列头部的下一个线程尝试获取锁。理解了AQS你就能明白为什么ReentrantLock和CountDownLatch的用法差异那么大但它们内部线程排队和唤醒的机制如此相似。面试被问AQS我建议从state、队列、CAS三个维度回答基本一次能讲完整。4.3 基于AQS的并发工具类实战对比光背AQS原理不够得会用它来解释工具类。CountDownLatch一个计数器调用await()的线程在等待计数器归零。典型场景是主线程等待多个子线程完成任务。注意它的计数器不可重置本质上是一次性的。CyclicBarrier和CountDownLatch相反的是它让多个线程互相等待到达屏障点后一起继续执行。循环屏障意味着可以重置复用。典型场景是多线程计算中间结果等待所有线程计算完第一个阶段再一起进入第二阶段。Semaphore信号量管理一定数量的许可。获取许可才能执行用完释放。典型场景是限流比如数据库连接池最多10个连接超过就排队等待。面试官特别喜欢问对比CountDownLatch和CyclicBarrier的区别。标准答法是CountDownLatch是主线程等待其他线程CyclicBarrier是多个线程互相等待前者是一次性的后者可循环使用。实际项目里的选型要看业务的等待关系方向。5. 线程池实战从参数配置到拒绝策略5.1 七个核心参数逐个拆解线程池是并发编程里最常用的工具也是面试最细碎的考点。ThreadPoolExecutor有七个参数一个一个讲corePoolSize核心线程数即使空闲也不会被回收。maximumPoolSize最大线程数当队列满且核心线程都在忙时最多能扩展到这么多。keepAliveTime非核心线程空闲存活时间。unit存活时间单位。workQueue任务等待队列。有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue等。threadFactory创建线程的工厂通常用来设置线程名、守护线程等。handler拒绝策略当队列满、线程数达到最大值时新任务的处理方式。执行流程要背清楚任务进来判断核心线程是否满了没满就建核心线程执行满了就放入队列队列满了判断线程数是否达到最大线程数没到就扩展非核心线程到了就执行拒绝策略。这一句话就是线程池的灵魂面试必考。5.2 四种拒绝策略怎么选Java提供了四种拒绝策略AbortPolicy直接抛RejectedExecutionException这是默认策略。适合认为任务不应该丢的场景。CallerRunsPolicy由提交任务的线程自己执行这个任务。相当于把压力反馈给调用方适合想降速但不丢任务的场景。DiscardPolicy直接丢弃新任务不报错。适合对任务丢失不敏感的场景。DiscardOldestPolicy丢弃队列里最旧的任务然后重新提交当前任务。我的实际建议是如果是核心业务不能丢任务用CallerRunsPolicy如果是可以容忍丢弃的非核心业务比如日志上报直接DiscardPolicy。生产环境里很少用默认策略因为未知异常会让调用方感知立断。5.3 核心线程数怎么定一个公式解决面试高频问线程池大小怎么配置这里给出一个可落地的策略分CPU密集型和IO密集型CPU密集型CPU核心数 1。因为CPU密集任务几乎不等待线程数超过核心数反而增加上下文切换开销。IO密集型CPU核心数 × 2或者经验公式线程数 CPU核心数 * (1 平均等待时间 / 平均计算时间)。IO密集任务大量时间在等待网络或磁盘可以通过更多线程把等待时间利用起来。如果不太好估算等待时间我的经验值核心数 × 2 起步然后压测曲线调优。不要迷信任何公式生产环境最终校准都要靠压测。5.4 线程池的关闭与常见坑线程池有两个关闭方法shutdown()等已提交任务执行完不再接受新任务和shutdownNow()中断正在执行的任务返回未执行的任务列表。用哪个取决于你需不需要优雅停机。常见的坑有三个第一个坑线程池中的线程抛出异常不会打印堆栈任务像“静默消失”。需要传入自定义ThreadFactory设置setUncaughtExceptionHandler或者用execute提交任务时在内部try-catch。第二个坑使用Executors.newFixedThreadPool()的无界队列LinkedBlockingQueue任务堆积可能导致内存爆掉。生产中我更推荐直接new ThreadPoolExecutor明确队列大小。第三个坑使用Executors.newCachedThreadPool()最大线程数是Integer.MAX_VALUE并发量大时会创建极多线程导致资源耗尽。6. 并发容器与工具类让代码直接跑得更稳6.1 ConcurrentHashMap的底层原理HashMap不是线程安全的Hashtable虽然线程安全但全表加锁性能太差ConcurrentHashMap是两者之间的最优解。JDK 1.8之后ConcurrentHashMap放弃了分段锁改用CAS synchronized实现。写操作时如果数组的某个槽位为空用CAS直接放入如果槽位不为空用synchronized锁住这个桶的链表或红黑树头节点。这样把锁的粒度从“段”细化到“桶”不同桶的写操作可以并行。另外它还有一个很重要的特性size()不是实时精确值而是估算值。因为在并发写的情况下想要精确统计大小需要全局锁代价太高。面试被问这个说明你有源码阅读经验。6.2 CopyOnWriteArrayList读多写少的武器CopyOnWriteArrayList是ArrayList的线程安全变体核心思想是“写时复制”每次修改add、set、remove都会复制一份底层数组在副本上修改修改完成后再把volatile的引用指向新数组。它的读操作不加锁所以非常适合读多写少的场景比如缓存黑白名单、监听器列表。但它有两个致命弱点写入时复制整个数组内存开销大数据存在短暂不一致读线程可能读到旧数据。面试被问适用场景时要主动说出这两个局限显得你用过而不是背过。6.3 BlockingQueue与生产者消费者模型BlockingQueue是并发场景下解耦的利器它支持在队列为空时阻塞获取、队列满时阻塞插入。常用的实现有ArrayBlockingQueue有界数组队列FIFO。LinkedBlockingQueue可选有界链表队列默认Integer.MAX_VALUE。SynchronousQueue不存数据的队列每个put必须等待一个take。在实际项目中用BlockingQueue做生产者和消费者之间的缓冲队列是一种非常稳定的架构模式。比如订单系统里生产者把订单消息丢入队列消费者按自己的处理能力消费这样削峰填谷的效果很理想。面试时写一个简单的生产者消费者模型核心代码很短但能体现你对线程通信的理解。6.4 ThreadLocal线程私有变量与内存泄漏ThreadLocal不会深究很容易翻车。它的原理是每个线程内部有一个ThreadLocalMapkey是当前ThreadLocal对象value是你要存的值。所以ThreadLocal变量天然是线程隔离的每个线程各存一份。最常见的使用场景简单DateFormat工具类非线程安全、拦截器里保存用户登录信息、数据库连接管理。最大坑是内存泄漏ThreadLocalMap的key是ThreadLocal的弱引用如果ThreadLocal被业务代码置为nullkey会被GC回收但value是强引用仍然存在于ThreadLocalMap中。如果线程长期存活比如线程池里的线程value就永远无法被回收导致内存泄漏。解决办法是使用完必须调用remove()。面试答到这一层才算真正懂ThreadLocal。7. 面试高频题与实战应对思路7.1 经典三连run vs start、sleep vs wait、notify vs notifyAll这三组对比几乎每场技术面试都会遇到回答要干脆、有层次。run vs start直接调用run只是执行普通方法不会启动新线程start会创建新线程由JVM调起线程后执行run方法。所以判断一个方法是否真的多线程执行关键看有没有调用start。sleep vs waitsleep是Thread静态方法不释放锁wait是Object实例方法释放锁。sleep必须捕获InterruptedExceptionwait不能在非同步代码块中调用。notify vs notifyAllnotify只随机唤醒一个线程notifyAll唤醒所有等待线程被唤醒的线程都要重新竞争锁。实际开发中推荐用notifyAll因为notify如果唤醒了一个不符合条件的线程导致它再次等待可能引发信号丢失。7.2 线程安全计数器一道题考穿三大特性面试官让你手写一个线程安全的计数器通常考察的不只是加锁能力更是性能意识。思路进阶路径第一版synchronized加锁保证安全但性能一般。 第二版用AtomicInteger通过CAS实现无锁安全性能有提升。 第三版读写分离场景下使用LongAdder它内部维护多个base和Cell数组把竞争分散到多个槽位超高并发下性能最好。这道题的加分回答是指出AtomicInteger在竞争激烈时CAS自旋可能导致性能下降而LongAdder通过分段思想把热点分散。面试官一听就知道你深入研究过。7.3 压死骆驼的最后一问如何在项目中排查并发问题除了八股题面试官会问一个开放性场景题CPU飙升怎么排查线程问题。我的标准流程第一步top找高CPU进程PID。 第二步top -Hp pid找具体高CPU线程的TID。 第三步把TID转成十六进制printf %x\n TID。 第四步jstack pid | grep 十六进制TID定位到具体线程栈。定位到线程栈后要看代码位置是在业务逻辑可能死循环或正则回溯还是在GC线程可能需要调堆内存。这套排查思路比单纯背并发知识更能让面试官眼前一亮。8. 学习路线与避坑指南从入门到精通的最后一公里8.1 推荐的学习路径与资源我给想系统学习Java并发编程的人一条清晰的路线不需要走弯路第一阶段基础入门掌握线程创建、生命周期、synchronized、volatile、wait/notify。目标是写线程安全的简单程序。第二阶段JUC工具应用掌握ThreadPoolExecutor参数、BlockingQueue、CountDownLatch、CyclicBarrier、Semaphore。目标是会用工具解决业务问题。第三阶段原理深挖读JMM相关文章理解CAS、AQS、ConcurrentHashMap源码。目标是能解释工具背后的设计思想。第四阶段工程实战用压测工具比如JMH做基准测试在真实项目里解决并发问题。这一步最关键因为只有踩了坑知识才真正变成能力。我个人不推荐一上来就啃源码会劝退。先把API玩熟对多线程程序有了直觉再去看原理就不难了。8.2 那些年我踩过的并发编程的坑最后分享几个我在真实项目里踩过的坑希望你别再踩一遍第一个坑用SimpleDateFormat在多线程环境下格式化日期导致解析出混乱的日期甚至抛异常。解决方案是全局用DateTimeFormatterJava 8之后线程安全或ThreadLocal包装。第二个坑在Spring的Async异步方法里直接使用this调用另一个异步方法结果异步失效。原因在于Spring异步代理只对外部调用生效内部调用走的是this对象而不是代理对象。第三个坑使用了无界队列的线程池公司一次营销活动把内存直接打爆导致整体服务不可用。从那之后我所有生产线程池都显式指定有界队列并设置拒绝策略。第四个坑用了ThreadLocal存储用户信息但忘记remove在长生命周期线程池中导致数据串线。那个问题排查了很久最后是在jmx里看到线程持有大量对象引用才发现。并发编程的学习没有捷径但有一条最有效的方式多写多线程的小demo多去模拟高并发场景多对着jstack分析问题。代码不会骗你踩过的坑都会成为你面试时的底气。如果你能坚持把这些知识点都过一遍并且亲手跑完文中的代码案例面试时“并发面试题”这一栏大概真的会成为你的送分题。