并发编程核心知识点速览

注意
本文最后更新于 2024-05-06,文中内容可能已过时。

整体逻辑:并发基础机制 → 锁核心原理 → 显式锁与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、高频读写共享变量。