JUC并发编程
JUC(java.util.concurrent)是Java并发编程的核心包,提供比synchronized更灵活强大的并发工具。本文涵盖volatile/CAS/AQS/线程池/并发容器/原子类/同步工具/CompletableFuture等核心知识点。
一、基础知识
1.1 并发三要素
|
要素 |
问题 |
解决方案 |
|---|---|---|
|
可见性 |
线程A修改的变量线程B看不到(CPU缓存) |
volatile、synchronized、Lock、final |
|
原子性 |
多条指令被打断(如i++读→改→写) |
synchronized、Lock、Atomic类(CAS) |
|
有序性 |
指令重排序导致意外结果 |
volatile(禁止重排)、synchronized(临界区内顺序)、final |
1.2 JMM(Java内存模型)
JMM定义多线程下共享变量访问规则。每个线程有本地内存(工作内存),操作共享变量时先拷贝到本地内存,写回主内存时间不确定。
- 8种原子操作:lock/unlock(锁)、read/load/use/assign/store/write(读写流程)
- happens-before原则:程序次序、volatile写-读、锁规则(unlock→lock)、传递性、线程start/join/interrupt规则等
二、volatile
2.1 三大特性
|
特性 |
机制 |
|---|---|
|
可见性 |
写volatile变量后立即刷新到主内存(store+write屏障);读之前从主内存加载(load+read屏障)。CPU缓存一致性协议(MESI)实现。 |
|
有序性 |
禁止指令重排序。LoadLoad屏障(读后不乱序)、StoreStore屏障(写前不乱序)、LoadStore屏障、StoreLoad屏障(最强,开销最大)。 |
|
不保证原子性 |
复合操作(i++)非原子:读→++→写,中间可能被其他线程打断。 |
2.2 DCL单例为何需要volatile
new对象三步(可能重排序):(a)分配内存 → (b)初始化对象 → (c)引用指向内存。若无volatile,(c)可能在(b)之前执行,另一个线程在①处判断instance非null但对象未初始化完毕,返回半成品对象。volatile禁止此重排序。
三、CAS(Compare And Swap)
3.1 原理
CAS(V, A, B):内存位置V,期望值A,新值B。仅当V的值=A时,将V更新为B并返回true;否则不修改返回false。通过CPU指令(lock cmpxchg)保证原子性。
3.2 CAS三大问题
|
问题 |
说明与解决 |
|---|---|
|
ABA问题 |
值从A→B→A,CAS检测不到中间变过。解决:AtomicStampedReference(版本号) / AtomicMarkableReference(布尔标记) |
|
自旋开销 |
CAS失败后不断重试消耗CPU。解决:自旋次数上限、自适应自旋、升级为重量级锁 |
|
仅保证单变量原子性 |
不能同时修改多个变量。解决:AtomicReference包装对象 + 不可变对象(每次CAS整个替换) |
四、synchronized 锁升级
JDK6起对synchronized进行大量优化,引入偏向锁→轻量级锁→重量级锁的锁升级机制(单向不可逆),大幅提升性能。
4.1 Mark Word
- 对象头中的Mark Word存储:hashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳
4.2 四种锁状态
|
锁状态 |
机制 |
|---|---|
|
无锁(01) |
无竞争,直接访问 |
|
偏向锁(01+threadId) |
对象"偏爱"第一个获取它的线程。该线程再次进入只需检查Mark Word中的Thread ID == 自身 → 直接获取。消除单线程重复加锁开销。 |
|
轻量级锁(00) |
线程在栈帧中创建Lock Record,CAS将对象头Mark Word替换为指向Lock Record的指针。自旋等待(自适应自旋)。 |
|
重量级锁(10) |
自旋等待仍失败或自旋中被抢占,锁膨胀为重量级锁。未获取锁的线程阻塞(OS Mutex),进入等待队列,不消耗CPU。 |
4.3 锁升级流程
- 初始无锁状态
- 第一个线程访问 → 偏向锁(CAS设置Thread ID),之后每次直接检查ID
- 第二个线程访问(轻微竞争)→ 撤销偏向锁 → 升级为轻量级锁,各线程自旋抢锁
- 自旋失败或竞争激烈 → 升级为重量级锁,未抢到的阻塞
4.4 其他优化
|
技术 |
说明 |
|---|---|
|
锁消除 |
JIT编译时分析,无逃逸的同步代码去掉锁(如StringBuffer在局部变量中使用) |
|
锁粗化 |
连续对同一对象加锁/解锁合并为一次范围更大的锁(如循环内synchronized→锁加在循环外) |
|
自适应自旋 |
自旋时间由前一次同一锁的自旋结果决定:上次自旋成功时间长则延长自旋,上次失败则缩短或直接阻塞 |
五、AQS(AbstractQueuedSynchronizer)
AQS是JUC最核心的框架,ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock等均基于AQS实现。
5.1 核心组成
- state:volatile int,同步状态(独占模式下0=未锁,1=已锁;共享模式表示剩余资源数)
- CLH队列变体:双向链表(FIFO),存储等待线程的Node。head是哑节点(已获取锁),tail指向队尾
- CAS:修改state和入队操作都用CAS,保证无锁竞争下的高效
- LockSupport.park/unpark:节点前驱是head且抢锁失败后park挂起;释放锁后unpark后继
5.2 独占模式流程(以ReentrantLock为例)
- lock():先尝试直接CAS修改state 0→1(非公平锁特有)→ 成功则设置owner=当前线程
- acquire(1):CAS失败进入acquire → tryAcquire(子类重写,如ReentrantLock会检查重入) → 失败则addWaiter(Node.EXCLUSIVE)入队CLH → acquireQueued:循环尝试,前驱是head时tryAcquire抢锁 → 失败则shouldParkAfterFailedAcquire检查前驱信号 → 前驱SIGNAL则parkAndCheckInterrupt挂起
- unlock():release(1) → tryRelease(state-1,若state=0则清空owner) → unparkSuccessor唤醒后继节点
5.3 共享模式流程(以Semaphore为例)
- acquireShared:tryAcquireShared(返回剩余资源数) → 失败入队 → 前驱为head时tryAcquireShared→ 失败park
- releaseShared:tryReleaseShared(state递增) → 成功后doReleaseShared唤醒后继 → 共享传播:唤醒后传递唤醒信号(setHeadAndPropagate),形成链式唤醒
5.4 AQS实现类一览
|
实现类 |
模式 |
state含义 |
|---|---|---|
|
ReentrantLock |
独占 |
0=未锁,≥1=锁次数(重入计数) |
|
ReentrantReadWriteLock |
独占+共享 |
高16位读锁计数,低16位写锁计数 |
|
Semaphore |
共享 |
剩余许可数 |
|
CountDownLatch |
共享 |
剩余计数(减到0唤醒所有等待线程) |
|
CyclicBarrier |
不直接基于AQS(用ReentrantLock+Condition) |
|
六、ReentrantLock
6.1 synchronized vs ReentrantLock
|
对比 |
synchronized |
ReentrantLock |
|---|---|---|
|
锁实现 |
JVM内置,C++实现 |
JDK实现,AQS框架 |
|
锁释放 |
自动(代码块/方法结束或异常) |
手动(必须finally中unlock) |
|
可中断 |
不可中断 |
lockInterruptibly()可响应中断 |
|
超时获取 |
不支持 |
tryLock(timeout, unit) 超时返回false |
|
公平性 |
非公平(固定) |
可选公平/非公平(构造参数) |
|
多条件 |
单个条件wait/notify |
多个Condition精准唤醒 |
|
性能 |
JDK6优化后差异很小 |
JDK6优化后差异很小 |
6.2 公平锁 vs 非公平锁
- 公平锁:lock()直接调用acquire(1)走CLH队列入队。严格FIFO,减少饥饿但吞吐量低(每次唤醒都要切换上下文)
- 非公平锁(默认):lock()先CAS抢一次,失败再入队。吞吐量高(减少上下文切换),但可能线程饥饿
6.3 Condition 条件变量
synchronized用wait/notify/notifyAll配合一个监视器锁实现等待通知。ReentrantLock通过newCondition()创建多个Condition,精准唤醒特定线程。底层用AQS的ConditionObject,每个Condition维护自己的单向等待队列。
- await():当前线程加入Condition等待队列 → 完全释放锁(state清空) → park挂起
- signal():从Condition队列头部移出一个节点到CLH同步队列 → unpark唤醒
- signalAll():将Condition队列所有节点转移到CLH同步队列
七、线程池
7.1 ThreadPoolExecutor 7个参数
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
|
参数 |
说明 |
|---|---|
|
corePoolSize |
核心线程数(常驻,除非allowCoreThreadTimeOut=true) |
|
maximumPoolSize |
最大线程数(达到时执行拒绝策略) |
|
keepAliveTime |
非核心线程空闲存活时间 |
|
workQueue |
阻塞队列(存放等待执行的任务) |
|
threadFactory |
线程工厂(命名线程,便于排查) |
|
handler |
拒绝策略 |
7.2 任务执行流程
- 提交任务 → 当前线程数 < corePoolSize → 直接创建核心线程执行任务(即使有空闲核心线程也先创建)
- 核心线程数已达上限 → 任务入队workQueue等待
- 队列已满 && 线程数 < maximumPoolSize → 创建非核心线程执行
- 线程数已达maximumPoolSize → 执行拒绝策略
7.3 4种拒绝策略
|
策略 |
行为 |
|---|---|
|
AbortPolicy(默认) |
抛RejectedExecutionException,中断任务提交 |
|
CallerRunsPolicy |
由提交任务的线程自己执行(自然形成背压,提交速率降低) |
|
DiscardPolicy |
直接丢弃任务,不抛异常(危险) |
|
DiscardOldestPolicy |
丢弃队列中最老的任务,重新提交当前任务 |
7.4 Executors 四种快捷线程池(均不推荐生产使用)
|
线程池 |
队列 |
风险 |
|---|---|---|
|
newFixedThreadPool(n) |
LinkedBlockingQueue(无界) |
队列无限堆积 → OOM |
|
newSingleThreadExecutor() |
LinkedBlockingQueue(无界) |
同上,队列无限堆积 → OOM |
|
newCachedThreadPool() |
SynchronousQueue(0容量) |
线程数无限增长(Integer.MAX_VALUE) → OOM |
|
newScheduledThreadPool(n) |
DelayedWorkQueue(无界) |
队列无限堆积 → OOM |
阿里巴巴规范:必须通过ThreadPoolExecutor构造函数显式指定参数,禁止使用Executors工厂方法。
7.5 线程池大小设置
- CPU密集型:核心线程数 = CPU核数 + 1。Runtime.getRuntime().availableProcessors()
- IO密集型:核心线程数 = CPU核数 × 2 或 CPU核数 ÷ (1 - 阻塞系数)。阻塞系数通常0.8~0.9
- 混合型:分开用两个线程池处理
7.6 线程池状态
- RUNNING → SHUTDOWN(shutdown()) → STOP(shutdownNow()) → TIDYING → TERMINATED
- SHUTDOWN不接收新任务但执行队列中的;STOP不接收新任务且中断正在执行的
八、原子类(Atomic系列)
8.1 分类
|
类别 |
实现类 |
|---|---|
|
基本类型 |
AtomicInteger、AtomicLong、AtomicBoolean |
|
数组类型 |
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray |
|
引用类型 |
AtomicReference、AtomicStampedReference(带版本号解决ABA)、AtomicMarkableReference(布尔标记) |
|
属性更新类型 |
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater(反射更新volatile字段,节省内存) |
|
累加器(JDK8+) |
LongAdder、DoubleAdder(高并发下优于AtomicLong);LongAccumulator、DoubleAccumulator(自定义运算) |
8.2 LongAdder 原理
AtomicLong在高并发下CAS频繁失败自旋,性能下降。LongAdder将热点分散到Cell数组:base + sum(Cell[]),每个线程CAS自己的Cell,减少竞争。最终sum()汇总base和所有Cell的值。
- 适用:高并发累加统计场景(计数器、QPS统计)
- 不适用:需要精确瞬时值的场景(sum()非原子操作)
九、同步工具类
9.1 CountDownLatch(倒计时门闩)
- 作用:一个线程等待N个线程完成后再执行。初始化设定计数,countDown()减1,await()阻塞直到计数归零
- 一次性:计数归零后不可重置
- 底层:AQS共享模式,state=计数。countDown→releaseShared;await→acquireSharedInterruptibly
9.2 CyclicBarrier(循环栅栏)
- 作用:N个线程相互等待,全部到达屏障后一起继续执行。可循环使用
- 回调:构造可传入barrierAction,最后一个到达的线程会执行此回调
- 底层:ReentrantLock + Condition(非直接基于AQS)。broken=true时重置
9.3 Semaphore(信号量)
- 作用:限制同时访问某资源的线程数量(限流)。acquire()获取许可,release()归还许可
- 公平/非公平:公平模式严格FIFO;非公平模式CAS抢许可
- 底层:AQS共享模式,state=许可数
9.4 三者对比
|
对比 |
CountDownLatch |
CyclicBarrier |
|---|---|---|
|
核心机制 |
一个等多个(或互相等) |
多方互相等待 |
|
可重用 |
否(计数归零就废了) |
可(计数归零自动重置) |
|
回调 |
无 |
可传Runnable回调 |
|
底层 |
AQS共享 |
ReentrantLock + Condition |
9.5 Exchanger
- 两个线程间交换数据。exchange(V data):线程A到达时若线程B已到则交换数据返回;否则A自旋等待B到达
9.6 Phaser(JDK7+)
- 增强版CyclicBarrier + CountDownLatch。支持多阶段同步、动态增减参与者、非阻塞arrive
十、ThreadLocal
10.1 原理
每个Thread内部维护一个ThreadLocalMap,key为ThreadLocal的弱引用,value为线程本地变量副本。get/set操作都是操作当前线程的ThreadLocalMap。
10.2 内存泄漏问题
- 原因:ThreadLocal为key的弱引用被GC回收后变成null,但value强引用仍在ThreadLocalMap中(Entry的key为null但value不为null),线程长期存活(线程池)逐渐堆积无法回收
- JDK8优化:get/set/remove时顺带清理key=null的Entry(expungeStaleEntry),但如果不主动调用这仨方法仍会泄漏
- 最佳实践:用完必须调remove()。线程池场景下调用方在finally中remove
10.3 InheritableThreadLocal
- 子线程可继承父线程的ThreadLocal值(Thread构造时copy InheritableThreadLocal值到子线程)
- 局限性:线程池场景下线程复用导致值不更新。替代:TransmittableThreadLocal(alibaba TTL,支持线程池传递)
十一、CompletableFuture(JDK8+)
异步编程利器。相比Future,CompletableFuture支持回调、链式组合、异常处理、多任务编排,强大得多。
11.1 创建方式
|
方法 |
说明 |
|---|---|
|
supplyAsync(Supplier) |
有返回值异步任务(默认ForkJoinPool,可传自定义线程池) |
|
runAsync(Runnable) |
无返回值异步任务 |
|
completedFuture(value) |
创建已完成的Future |
11.2 任务编排
|
方法 |
作用 |
|---|---|
|
thenApply |
依赖前一步结果转换(同步,同线程) |
|
thenApplyAsync |
同上,异步线程 |
|
thenAccept |
消费前一步结果,无返回(Consumer) |
|
thenRun |
前一步完成后执行,不关心结果 |
|
thenCompose |
扁平化组合(连接两个依赖的异步任务,避免Future嵌套) |
|
thenCombine |
合并两个独立任务结果(BiFunction) |
|
thenAcceptBoth |
消费两个任务结果无返回 |
|
applyToEither |
两个任务谁快用谁的结果 |
|
allOf |
等待所有任务完成(返回Void) |
|
anyOf |
任一任务完成即返回 |
11.3 异常处理
- exceptionally:相当于catch,发生异常时提供默认值
- handle:相当于finally(无论是否异常都执行,有result和exception两个参数)
- whenComplete:消费结果+异常,不修改返回值
十二、Fork/Join 框架
12.1 核心思想
- 分治:大任务递归拆分为小任务 → 并行执行 → 合并结果
- 工作窃取(Work Stealing):空闲线程从繁忙线程的双端队列尾部窃取任务执行。ForkJoinPool每个线程维护一个WorkQueue
12.2 使用方式
- 继承RecursiveTask<V>(有返回值)或 RecursiveAction(无返回值),重写compute()
- ForkJoinPool.commonPool()(JDK8默认并行流线程池,核数-1个线程)
面试高频TOP10:
- synchronized 锁升级过程(偏向→轻量→重量)
- AQS原理(state + CLH队列 + CAS)
- ReentrantLock vs synchronized 对比
- 线程池7参数和执行流程
- volatile 三大特性 + DCL单例
- CAS原理 + ABA问题
- ThreadLocal 内存泄漏
- CountDownLatch vs CyclicBarrier 对比
- LongAdder 原理(分散热点)
- CompletableFuture 异步编排