Java性能优化多线程篇

Java性能优化 · 多线程篇

多线程性能调优围绕三条主线展开:

优化维度 核心问题 主要手段
锁优化 如何降低锁竞争与持有成本 锁升级、锁分离、锁分段、CAS 乐观锁
上下文切换 减少线程被切出/切入的开销 减少阻塞、协程、合理线程池
资源容器 不同场景选择最合适的并发容器 ConcurrentHashMap / SkipListMap / CopyOnWriteArrayList / 阻塞队列

支撑以上三者的基础是:线程池的合理设置对 Java 内存模型 / 一致性模型的理解


锁优化

Synchronized

JDK1.5 及之前,Synchronized 性能很差,因为官方未对其实现进行相应的优化,JDK1.6 起其性能得到了明显的提升。

实现原理

  • 修饰代码块:编译为 monitorenter + monitorexit 指令,依赖对象的 Monitor(ObjectMonitor)。
  • 修饰方法:通过 ACC_SYNCHRONIZED 访问标志实现。
  • 每个 Java 对象都关联一个 Monitor,线程获取 Monitor 即获得锁,未获取到的进入 _EntryList 阻塞,调用 wait() 的进入 _WaitSet

锁升级路径(JDK1.6 引入,核心优化)

1
无锁 → 偏向锁 → 轻量级锁(自旋) → 重量级锁
锁状态 适用场景 关键机制
偏向锁 同一线程多次获取锁 对象头 Mark Word 记录线程 ID;高竞争下应关闭(-XX:-UseBiasedLocking),自JDK15 已经默认禁用
轻量级锁 多线程交替执行、竞争不激烈 CAS 修改 Mark Word 中的指针
自旋锁 锁占用时间极短 CAS 循环重试;JDK1.7 默认开启;占用 CPU
重量级锁 竞争激烈、锁占用时间长 线程挂起阻塞;用户态/内核态切换

编译期优化(JIT)

  • 锁消除:通过逃逸分析确认锁对象只被一个线程访问,则消除 synchronized 字节码。
  • 锁粗化:相邻同步块使用同一锁时,合并为一个大的同步块,减少反复申请/释放。

减小锁粒度

  • 把数组/队列拆分为多个小对象,让竞争分散。
  • 经典案例:JDK1.7 的 ConcurrentHashMap 通过 Segment(分段锁)降低锁粒度;JDK1.8 改为 Synchronized + CAS,结构更简单。

核心结论:减少锁竞争是优化 Synchronized 的关键。尽量让锁停留在轻量级锁,通过减小锁粒度、减少锁持有时间来达成。


Lock 同步锁

与 Synchronized 的对比

  • Synchronized:JVM 隐式加锁/释放;锁升级到重量级锁后性能不稳定。
  • Lock:Java 层显式加锁/释放,基于乐观锁实现;但线程阻塞时仍会挂起,本质上仍属悲观锁。

AQS 框架

AbstractQueuedSynchronizer(AQS)是 Lock 的实现基础:

  • state 变量:表示加锁状态(ReentrantLock 中代表重入次数)。
  • CLH 等待队列:基于链表实现,所有阻塞线程在此排队;所有操作通过 CAS 完成。

锁分离优化 —— 读写锁

ReentrantReadWriteLock(RRW)通过高低位巧妙设计 state:

  • 高 16 位:读锁计数
  • 低 16 位:写锁计数

特点:

  • 多个读线程可并发;读读不互斥。
  • 写线程与读/写线程都互斥。
  • 缺点:在读多写少场景下,写线程可能遭遇饥饿(Starvation)

StampedLock(JDK1.8)是用来解决 RRW 读多写少时写线程饥饿问题的,它有三种模式:

模式 特点
写锁 独占,返回 stamp
悲观读 共享,返回 stamp
乐观读 无锁读取获取 stamp,操作后用 validate(stamp) 二次校验;失败则升级为悲观读

优势:乐观读无 CAS 竞争(直接读取 stamp 的值),效率高于 RRW。

缺陷(导致未被广泛使用)

  • 不支持重入(嵌套调用易死锁)
  • 不支持条件变量 Condition
  • 写多读少场景下性能无优势

乐观锁(CAS)

核心算法 CAS

三个参数:V(变量当前值)、E(预期值)、N(新值)。仅当 V == E 时才将 V 置为 N,否则返回 V 的真实值。

底层基于 CPU 指令,由处理器提供总线锁定缓存锁定两种机制保证原子性。

JDK 中的实现

  • java.util.concurrent.atomic.* 全部基于 CAS(AtomicIntegerAtomicLong)。
  • Unsafe 类调用 CPU 底层指令完成原子操作。

CAS 的问题与优化

  • ABA 问题:值从 A 变 B 又变回 A,CAS 误判为未变。解决:引入版本号(AtomicStampedReference)。
  • 高并发写竞争激烈:大量线程 CAS 失败并循环重试,占用大量 CPU。

LongAdder(JDK1.8)的解决思路

  • 空间换时间:将单一共享变量的写压力分散到 Cell[] 数组的不同槽上。
  • 读时:合并 base + 各 cell,可能为近似值,但最终值准确。
  • 适合:高并发写场景、统计型业务(如全局计数)。
  • 不适合:对实时性要求极高的场景(仍用 AtomicLong)。

三种锁性能对比结论

场景 最优选择
读 >> 写 ReentrantReadWriteLock / StampedLock / 乐观锁
写 >> 读 乐观锁(LongAdder)性能最佳,其它四者差不多
读 ≈ 写 两种读写锁 + 乐观锁优于 Synchronized / ReentrantLock

上下文切换调优

上下文切换基础

定义:线程由 RUNNING 转为 BLOCKED,再回到 RUNNABLE 被调度执行的过程。需保存/恢复:

  • CPU 寄存器内容(已执行/正在执行/即将执行的任务)
  • 程序计数器内容(当前指令位置 + 下一条指令位置)

诱因分类

类型 触发操作/原因
自发性 sleep()wait()yield()join()park()synchronizedlock
非自发性 时间片用完、GC(Stop-The-World)、线程优先级

监控工具

工具 适用平台 用途
vmstat 1 3 Linux 查看系统整体 cs(context switch)次数
pidstat -w -p <pid> -t Linux 查看指定进程每个线程的 cswch/s、nvcswch/s
Process Explorer Windows 图形化查看线程上下文切换
jstack <pid> 跨平台 导出线程堆栈信息,定位 BLOCKED 死锁

优化上下文切换

核心观点:在多线程编程中,锁不是性能开销的根源,竞争锁才是

优化方向一:减少竞争锁

  1. 减少锁持有时间:将无关代码移出同步块,特别是 I/O 与阻塞操作。
  2. 降低锁粒度
    • 锁分离:读写锁。
    • 锁分段:ConcurrentHashMap(JDK1.7)。
  3. CAS 替代锁Atomic*volatile 读写不会引起上下文切换。

优化方向二:wait/notify 优化

wait/notify 至少引起 3 次上下文切换,且 notifyAll() 易过早唤醒导致线程再次阻塞。

优化策略:

  • **优先用 notify() 替代 notifyAll()**,避免唤醒无关线程。
  • 通知后尽快释放锁,减少被唤醒线程的等待。
  • 使用 Lock + Condition 替代 synchronized + wait/notify
    • 可解决 wait(long) 无法区分”超时 vs 被通知”的问题。
    • 可解决过早唤醒的问题。

优化方向三:合理线程池大小

避免 Executors.newCachedThreadPool() 等会无限创建线程的池——大量长任务时会产生数百个线程,引发频繁上下文切换。

优化方向四:轻量级线程

核心就是轻量级线程,第六节有介绍。

优化方向五:减少 GC 频率

GC(特别是 Full GC)会触发 Stop-The-World。减少对象分配、合理设置堆分区可降低上下文切换。

监控建议:把上下文切换(cs)作为服务性能指标纳入监控,及时发现异常。


并发容器的选择

Map 容器

容器 一致性 数据结构 适用场景
Hashtable 强一致 数组+链表 强一致需求,但并发性能差(方法全用 synchronized 修饰)
ConcurrentHashMap 弱一致 数组+链表/红黑树 首选;JDK1.7 分段锁 → JDK1.8 Synchronized + CAS
ConcurrentSkipListMap 弱一致 跳表 大数据量、写多读少;按 key 排序;O(log n) 平均复杂度

跳跃表(SkipList)要点

  • 通过多层索引实现”空间换时间”。
  • 增删基于 CAS,并发性能优于红黑树(树的操作涉及大量节点)。
  • 在非线程安全的 Map 中,TreeMap(红黑树)与 SkipListMap 单线程性能相当。

List 容器

容器 机制 适用场景
Vector synchronized 修饰所有方法 读写都加锁,性能差;读远大于写时不推荐
CopyOnWriteArrayList 读无锁,写时复制新数组 读远大于写、写少;如黑名单、配置中心;数据一致性要求最终一致即可

队列

阻塞队列(用于线程池、生产者-消费者模型)

队列 数据结构 特性
ArrayBlockingQueue 数组 有界,FIFO,ReentrantLock + Condition
LinkedBlockingQueue 链表 有界,FIFO,吞吐量通常高于 Array
PriorityBlockingQueue 二叉堆 无界(MAX_VALUE - 8),按优先级
DelayQueue 基于 PriorityBlockingQueue 支持延时获取元素
SynchronousQueue 不存储元素 每次 put 必须等 take;可指定 FIFO 或 LIFO

非阻塞队列

  • ConcurrentLinkedQueue:基于链表 + CAS 实现的无界线程安全队列,适合高并发排队(如抢购);注意内存溢出风险。

线程池调优

线程池原理

Java 线程(JDK21之前)采用 1:1 线程模型:每个 Java 线程映射到一个内核线程(通过 pthread_createclone() 创建)。线程的创建/销毁有系统开销,线程池通过复用减少开销。

Executor 框架

推荐使用 ThreadPoolExecutor 自定义线程池,而非 Executors 工厂类(默认参数不利于调优)。

核心参数

1
2
3
4
5
6
7
8
9
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)

默认工作流程

  1. 线程数 < corePoolSize → 创建新线程执行。
  2. 线程数 ≥ corePoolSize → 任务进入队列。
  3. 队列满 → 创建非核心线程,直到达到 maximumPoolSize。
  4. 已达 maximumPoolSize 且队列满 → 触发拒绝策略(默认抛 RejectedExecutionException)。

注意点

  • 预热:prestartAllCoreThreads() 提前创建核心线程(抢购场景常用)。
  • 核心线程也可回收:通过 allowCoreThreadTimeOut(true)

线程数计算公式

任务类型 推荐公式
CPU 密集型 N + 1(N 为 CPU 核数;+1 应对偶发缺页中断)
I/O 密集型 2N
混合型(通用) N * (1 + WT/ST),其中 WT = 线程等待时间,ST = 线程运行时间

注意这只是理论推荐值,生产环境要根据压测结果配置。

核心原则

  1. 先保证合理线程数使 CPU 利用最大化。
  2. 再通过有界队列(防止 OOM)缓存来不及处理的任务。
  3. 不同任务类型使用不同线程池,避免相互影响;核心业务应单独部署。

Amdahl 定律

S = 1 / (1 - a + a/n)

系统性能提升的瓶颈在于串行部分。优化并行段只是缩短 a/n 这一项;优化串行部分(如减少锁持有时间、避免阻塞 I/O)才能真正提升整体性能。


轻量级线程

线程实现模型

模型 关系 优缺点
1:1(Java) 用户线程 : 内核线程 = 1 : 1 简单,但创建/切换有用户态/内核态切换开销
N:1 多用户线程对应 1 内核线程 切换快,但一个线程系统调用会阻塞整个进程
N:M(Go) N 用户线程映射到 M 内核线程 灵活,兼顾性能与并发能力

Java 早期在 Linux 下基于 pthread_createclone() 实现 → 1:1 模型,JDK21 引入了轻量级用户线程(fibers),并行能力有了极大的提升。

协程的本质

  • 比线程更轻量,由程序(用户态)调度,不进入内核。
  • 可看作”可暂停的函数”或”线程上的代码块”。
  • 挂起时让出执行权,唤醒后从挂起点继续。
  • 同一线程上可运行成千上万个协程。

数据一致性

分布式系统的很多问题和多线程领域非常类似,一个经典的例子就是 Happens-Before。

Java 内存模型(JMM)核心

  • 局部变量 → 线程栈(线程私有,无需关心一致性)。
  • 共享变量 → 堆/方法区(线程共享)。
  • 工作内存:CPU 高速缓存(L1/L2/L3),每核一份。

多核问题:两个 CPU 核心同时缓存同一共享变量时,各自修改后写回主存可能产生数据不一致。

重排序

编译器/CPU 为优化性能可能改变指令执行顺序。在单线程下不影响结果,但在并发下可能引发问题。

Happens-Before 规则

为约束线程执行顺序,JMM 定义了 8 条 Happens-Before 规则:

规则 内容
程序次序规则 单线程内执行结果与顺序执行一致
锁定规则 unlock 先于后续对同一锁的 lock
volatile 变量规则 volatile 写先于后续读
线程启动规则 start() 先于线程内任何动作
线程终结规则 线程内所有操作先于 terminate() 检测
对象终结规则 对象初始化完成先于 finalize()
传递性 A hb B,B hb C ⇒ A hb C
线程中断规则 interrupt() 调用先于中断事件检测

一致性

级别 含义 典型实现
严格一致性(强一致) 任何时刻各线程读到的缓存数据完全一致;全局时钟序 Hashtablesynchronized
顺序一致性 单线程内执行有序;保证读到最近一次写入 volatile(阻止重排序)
弱一致性 不保证立刻读到最近写入,但能保证最终读到 单写锁 + 无锁读、ConcurrentHashMap

ConcurrentHashMap 为什么是弱一致?

  • get() 不加锁,可能读到尚未刷新到主存的数据。
  • size()clear()foreach 等方法也都未完全加锁,存在数据不确定性。

关于一致性,在分布式系统中有线性一致性、顺序一致性、因果一致性等说法。

多线程优化的本质是减少竞争、减少切换、减少等待。锁要选对、粒度要小、容器要匹配场景、线程池要合理,最终都要靠压测数据而非经验值来落地。


Java性能优化多线程篇
https://zhuwenjie0716.github.io/2026/07/05/Java性能优化多线程篇/
作者
Wenjie Zhu
发布于
2026年7月5日
许可协议