并发编程核心知识点速览
整体逻辑:并发基础机制 → 锁核心原理 → 显式锁与AQS → 线程管理 → 线程池 → 并发工具类 → 线程通信与隔离 → 并发故障与排查 → 拓展点
一、并发基础机制
1.1 Volatile 关键字
核心作用:轻量级无锁并发机制,开销极低,仅保障可见性、有序性,不保证原子性。
三大核心特性
-
保证可见性:线程修改volatile变量后,强制刷新至主内存,同时失效其他线程的本地缓存,保证多线程数据实时同步。
-
禁止指令重排序:限制编译器、CPU的指令优化重排,保证代码执行顺序与编码顺序一致。
-
不保证原子性:仅支持单次读写原子,无法保障i++等「读取-修改-写入」复合操作的原子性。
底层原理:内存屏障
-
写屏障:volatile写入后触发,强制数据落主内存,禁止前置指令重排至后置
-
读屏障:volatile读取前触发,强制清空本地缓存,禁止后置指令重排至前置
1.2 DCL 双重检查锁 + Volatile
问题根源:对象创建原生三步:分配内存 → 初始化对象 → 引用赋值。CPU指令重排可能打乱为:分配内存 → 引用赋值 → 初始化对象。
多线程场景下会产生半初始化对象,外部线程获取未完成初始化的实例,引发隐性线程安全BUG。
volatile核心作用:禁止对象创建指令重排序,严格保证对象初始化完成后再赋值引用,彻底解决DCL线程安全问题。
面试总结:DCL不加volatile不会直接报错,但存在隐性并发漏洞,是单例模式必备修饰符。
1.3 CAS、ABA问题与Atomic原子类
CAS(比较并交换):CPU硬件级乐观无锁机制,核心参数:内存值V、预期值A、新值B。内存值与预期值一致则更新数据,不一致则自旋重试。
优缺点:用户态执行、无线程阻塞、开销小;但自旋消耗CPU、仅支持单变量原子操作,存在ABA问题。
ABA问题:线程读取变量值为A,其他线程完成A→B→A的修改还原,CAS无法识别中间改动,判定数据无变更,导致数据异常。
解决方案:新增版本号/时间戳校验,使用 AtomicStampedReference。
Atomic原子类:基于CAS+自旋封装,保障单个变量原子操作。高并发激烈竞争场景自旋损耗CPU,优先使用锁替代。
二、锁核心原理
2.1 Synchronized 锁升级机制
JDK1.6 优化后,synchronized 锁单向升级、不可降级,完整流程:偏向锁 → 轻量级锁 → 重量级锁
-
偏向锁(无竞争):默认开启,对象头绑定固定线程ID,单线程重复加锁无需额外操作,零开销,适配单线程高频加锁场景。
-
轻量级锁(交替竞争):多线程交替抢锁、无并发冲突,基于CAS自旋抢锁,用户态运行,无线程阻塞,开销较小。
-
重量级锁(高并发竞争):CAS自旋失败,线程进入内核阻塞队列,线程挂起等待,系统开销最大。
配套优化机制
-
锁撤销:偏向锁长期无竞争,JVM自动撤销偏向标记,恢复无锁状态,回收资源。
-
批量重偏向:同类对象频繁切换锁持有者,达阈值20,批量修改对象偏向线程,避免逐个撤销的性能损耗。
-
批量撤销:锁竞争极度频繁,达阈值40,直接关闭当前类所有对象的偏向锁,默认走轻量级锁。
核心关键点:偏向锁绑定线程ID,不关注锁是否释放;其他线程抢锁即触发升级,且锁升级后无法回退为偏向锁。
2.2 自适应自旋锁
适配于synchronized轻量级锁、AQS底层,自旋次数不固定,由JVM动态适配:
-
过往自旋抢锁成功,判定竞争小,自动增加自旋次数
-
过往自旋频繁失败,判定竞争激烈,终止自旋,直接升级重量级锁
作用:平衡CPU自旋开销与锁竞争成本,适配不同并发场景,优化轻竞争性能。
2.3 锁降级
synchronized 仅支持锁升级,无锁降级能力;仅 ReentrantReadWriteLock 读写锁 支持锁降级。
降级流程:持有写锁 → 获取读锁 → 释放写锁(写锁降级为读锁)
核心价值:保障数据可见性,避免读写竞争,提升并发吞吐量。
三、AQS 与显式锁
3.1 AQS 核心原理
AQS是Java所有显式锁的底层基石,核心三要素:state状态值 + 双向FIFO阻塞队列 + park/unpark阻塞唤醒
-
state:原子整型变量,CAS修改,不同组件含义不同:ReentrantLock为重入次数、CountDownLatch为计数器、Semaphore为剩余资源数
-
双向阻塞队列:抢锁失败的线程封装为Node入队,避免无限自旋消耗CPU
-
虚拟头节点:空哨兵节点,不绑定线程;让队列永久非空,消除大量判空逻辑,统一线程入队、唤醒规则
-
park/unpark:调用操作系统内核,实现线程挂起唤醒,存在用户态、内核态切换开销
执行流程:线程CAS抢锁成功则执行业务;失败则入队park阻塞;锁释放后unpark唤醒后继线程。
锁类型:独占锁(ReentrantLock,单次唤醒单线程)、共享锁(CountDownLatch/Semaphore,批量唤醒线程)
3.2 ReentrantLock 可重入锁
基于AQS实现,特性全面、可控性强,核心能力:可重入、公平/非公平、可中断、可超时、多条件队列。
-
可重入:同一线程可重复加锁,state计数累加,必须清零才算完全释放锁
-
非公平锁(默认):新线程直接CAS抢锁,吞吐量高,可能出现线程饥饿
-
公平锁:严格遵循队列FIFO,杜绝线程饥饿,吞吐量偏低
ReentrantLock vs synchronized
-
synchronized:JVM原生、自动解锁、使用简单、仅非公平、可控性差
-
ReentrantLock:代码层实现、手动unlock、功能丰富、适配复杂并发场景
-
JDK1.6后两者性能基本持平
四、线程核心管理
4.1 线程五大状态与流转
NEW:线程新建,未调用start(),未启动
RUNNABLE:包含就绪、运行状态,争抢CPU执行权
BLOCKED:阻塞等待,争抢synchronized排他锁失败
WAITING:无限阻塞(wait、join、park),需手动唤醒
TIMED_WAITING:限时阻塞(sleep、超时wait),超时自动唤醒
TERMINATED:线程任务执行完毕,彻底终止
4.2 Sleep / Wait / Join 区别
-
sleep:Thread静态方法,不释放锁,超时自动唤醒,仅用于线程休眠
-
wait:Object成员方法,立即释放锁,需notify唤醒,用于多线程通信
-
join:Thread成员方法,底层依赖wait,释放锁,等待目标线程执行完毕
极简口诀:sleep抱锁睡,wait放锁等,join等线程结束。
4.3 线程中断机制
Java废弃stop()暴力终止线程,通过中断标记位实现优雅停机。
-
interrupt():仅修改中断标记,不直接终止线程
-
isInterrupted():查询中断标记,不清除标记
-
Thread.interrupted():查询中断标记,自动清除标记
核心规则:阻塞态线程触发中断会抛异常并清空标记;运行态线程仅标记中断,需开发者手动处理终止逻辑。
4.4 守护线程
Java线程分为两类:
-
用户线程:业务主线程,JVM需等待所有用户线程结束才会退出
-
守护线程:后台依附线程(如GC线程),所有用户线程结束后,守护线程随JVM直接退出
使用规则:start()之前调用setDaemon(true)设置;适用于日志监控、心跳巡检等后台任务。
4.5 Thread.sleep(0) 作用
不休眠线程,核心作用是触发CPU时间片重分配,主动让出剩余CPU时间片,让操作系统重新调度线程,平衡并发调度公平性,避免单线程霸占CPU。
五、线程池核心体系
5.1 七大参数
核心线程数、最大线程数、空闲超时时间、时间单位、阻塞队列、线程工厂、拒绝策略
5.2 执行流程
核心线程未满 → 创建核心线程执行任务 → 核心线程已满,任务入队 → 队列已满,创建非核心线程 → 线程数达上限+队列满,触发拒绝策略
5.3 四种拒绝策略
-
AbortPolicy(默认):直接抛出异常
-
CallerRunsPolicy:调用者线程自行执行任务,实现限流
-
DiscardPolicy:静默丢弃新任务,无报错
-
DiscardOldestPolicy:丢弃队列最旧未执行任务,执行新任务
5.4 Executors 工具类弊端
阿里规范禁止使用Executors创建线程池:Fixed/Single线程池无界队列导致任务堆积OOM;Cached线程池无最大线程限制,引发线程爆炸、CPU打满。生产环境必须手动配置七大参数。
5.5 CPU/IO密集型参数配置
-
CPU密集型:多计算、少阻塞,线程数=CPU核心数+1,少线程、大队列,减少上下文切换
-
IO密集型:多阻塞、少计算,线程数=CPU核心数*2,适度多开线程,禁止无界队列
六、并发工具与异步编排
6.1 三大并发工具类
-
CountDownLatch 倒计时门闩:一次性不可重置,多子线程执行完毕,主线程汇总放行
-
CyclicBarrier 循环栅栏:可循环复用,多线程凑数同步,统一批量执行
-
Semaphore 信号量:限制并发线程数量,用于接口限流、资源抢占控制
6.2 CompletableFuture 异步编排
JDK8+ 异步编程工具,修复Future阻塞轮询弊端,支持链式调用、任务自由编排。
-
基础创建:runAsync(无返回)、supplyAsync(有返回)
-
串行执行:thenApply、thenAccept、thenRun
-
结果合并:thenCombine(双任务合并)
-
批量编排:allOf(全部执行)、anyOf(任意一个执行)
业务场景:多接口并行调用、接口聚合查询、异步解耦,优化接口响应速度。
七、线程隔离与内存泄漏
7.1 ThreadLocal
核心作用:实现线程私有变量,完成多线程数据隔离,解决共享变量并发冲突。
底层结构:线程内置ThreadLocalMap,key为ThreadLocal(弱引用),value为业务数据(强引用)。
7.2 内存泄漏问题
泄漏原因:线程池线程常驻不销毁,ThreadLocal外部引用失效后,弱引用key被GC回收(key=null),但强引用value无法回收,长期堆积造成内存泄漏。
解决方案:使用完毕必须手动调用remove(),线程池复用线程场景必不可少。
弱引用意义:尽可能回收ThreadLocal对象,降低内存泄漏程度,无法彻底根治。
八、并发故障、排查与优化
8.1 死锁
四大必要条件(缺一不可):互斥、请求保持、不可剥夺、循环等待
典型场景:双线程嵌套获取两把不同锁,互相持有对方所需锁,形成循环等待。
排查方式:jps查询进程ID → jstack 进程ID,检索deadlock关键字定位死锁代码。
规避方案:统一锁获取顺序、使用tryLock超时抢锁、减少锁嵌套、缩小锁范围。
8.2 伪共享
原理:CPU以64字节CacheLine缓存行缓存数据,多个独立共享变量共存同一缓存行,单个变量修改会导致整行缓存失效,引发CPU缓存颠簸、并发性能下降。
解决方案:缓存行填充,使用JDK8 @Contended 注解解决。
常见场景:并发计数器、AtomicLong、高频读写共享变量。