多线程(2)
多线程(2)
🔴🟠🟡🟢🔵🟣🔴🔴🟠🟡🟢🔵🟣🔴🔴🟠🟡🟢🔵🟣🔴🔴🟠🟡🟢🔵🟣🔴
ThreadLocal什么时候会出现OOM的情况?为什么?
ThreadLocal 导致 OOM 的完整解析
ThreadLocal 是 Java 中用于实现线程本地存储的核心工具,但其设计中隐含的内存管理陷阱可能导致 内存溢出(OOM)。本文将结合 Thread
、ThreadLocal
、ThreadLocalMap
的源码,深入分析 OOM 的触发条件、底层逻辑,并给出解决方案。
一、ThreadLocal 的核心架构:Thread、ThreadLocal、ThreadLocalMap 的关系
ThreadLocal 的核心设计目标是 为每个线程维护独立的变量副本,其底层依赖三个关键组件:
组件 | 角色描述 |
---|---|
Thread 类 | 每个线程实例(Thread 对象)内部维护两个 ThreadLocalMap 字段: - threadLocals :存储当前线程的普通 ThreadLocal 变量 - inheritableThreadLocals :存储可继承的 ThreadLocal 变量(默认不启用) |
ThreadLocal<T> | 用户使用的 API 类(如 threadLocal.set(value) ),本质是 ThreadLocalMap 的 Key |
ThreadLocalMap | 真正存储数据的容器(类似 HashMap),每个 Thread 实例独立拥有一个 ThreadLocalMap |
1. Thread 类的源码:存储 ThreadLocalMap
Thread
类的源码(JDK 8)中,threadLocals
和 inheritableThreadLocals
是存储线程局部变量的核心字段:
public class Thread implements Runnable {// 存储普通 ThreadLocal 变量的哈希表(用户常用)ThreadLocal.ThreadLocalMap threadLocals = null;// 存储可继承 ThreadLocal 变量的哈希表(通过 InheritableThreadLocal 访问)ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;// 线程终止时清理资源(关键方法)private void exit() {if (group != null) {group.threadTerminated(this);group = null;}// ... 其他资源清理(如栈、上下文等) ...threadLocals = null; // 清空普通 ThreadLocal 变量inheritableThreadLocals = null; // 清空可继承 ThreadLocal 变量}
}
threadLocals
:用户通过ThreadLocal.set()
存储的变量会存入此哈希表。exit()
方法:线程终止时调用,清空threadLocals
和inheritableThreadLocals
,释放内存。
2. ThreadLocalMap 的源码:存储线程局部变量的容器
ThreadLocalMap
是 ThreadLocal
的静态内部类,本质是一个自定义的哈希表,源码核心结构如下:
static class ThreadLocalMap {// Entry 数组,存储键值对(初始容量 16)private Entry[] table;// 扩容阈值(容量 * 负载因子,默认负载因子 0.75)private int threshold;// 构造函数(初始化数组和阈值)ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY]; // INITIAL_CAPACITY = 16int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY); // 阈值 = 16 * 0.75 = 12}// Entry 定义(继承弱引用)static class Entry extends WeakReference<ThreadLocal<?>> {Object value; // 线程的局部变量副本(强引用)Entry(ThreadLocal<?> k, Object v) {super(k); // Key 是弱引用(指向 ThreadLocal 实例)value = v; // Value 是强引用(指向线程的局部变量)}}
}
Entry
类:继承自WeakReference<ThreadLocal<?>>
,其 Key 是弱引用(指向ThreadLocal
实例),Value 是强引用(指向线程的局部变量)。- 弱引用 Key 的意义:当
ThreadLocal
实例不再被外部引用时(如开发者主动移除或线程结束),Entry
的 Key 会被 GC 标记为可回收,避免内存泄漏。
二、ThreadLocal 的内存回收机制:为什么可能泄漏?
ThreadLocal 的内存回收依赖两个层面:Key 的回收(ThreadLocal 实例) 和 Value 的回收(线程的局部变量)。理解这两个过程是定位 OOM 的关键。
1. Key 的回收:弱引用与 GC
Entry
的 Key 是弱引用(WeakReference<ThreadLocal<?>>
),因此:
- 当
ThreadLocal
实例(如用户定义的threadLocal1
)不再被任何强引用指向时(例如开发者代码中不再持有该变量),GC 会回收 Key(将其标记为null
)。 - 此时,
Entry
变为 无效条目(Key 为null
,但 Value 仍被强引用)。
2. Value 的回收:惰性清理机制
无效条目中的 Value 无法直接被 GC 回收(因为被 Entry 强引用),必须通过 ThreadLocalMap
的清理机制主动清除。清理触发时机包括:
- 调用
ThreadLocal.get()
:若发现当前 Key 对应的 Entry 已失效(Key 为null
),会触发清理。 - 调用
ThreadLocal.set()
:插入新 Entry 前,会清理当前哈希位置附近的无效条目。 - 调用
ThreadLocal.remove()
:直接删除当前 Key 对应的 Entry(最彻底的清理方式)。 - 线程终止时:
Thread.exit()
方法会清空threadLocals
,释放所有 Entry。
清理的局限性:惰性且不彻底
ThreadLocalMap
的清理是 惰性清理(Lazy Cleanup),仅在特定操作时触发,且每次清理可能只处理部分无效条目(而非全部)。例如:
set()
方法中,插入新 Entry 前仅清理当前哈希位置附近的无效条目(expungeStaleEntry
)。get()
方法中,若发现 Key 为null
,仅清理当前 Entry,不会遍历整个数组。
问题根源:如果开发者未主动调用 remove()
,且线程长期存活(如线程池中的线程),无效条目会持续累积,导致 Value 无法释放,最终引发 OOM。
三、OOM 的核心场景:线程池的长期存活线程
线程池(如 FixedThreadPool
)的核心线程是 复用且长期存活 的(除非线程池被显式销毁)。结合 ThreadLocal 的清理机制,线程池会放大内存泄漏问题。
1. 线程池的线程生命周期
线程池(如 Executors.newFixedThreadPool(1)
)创建的线程会重复执行多个任务(Runnable
),线程生命周期远长于单个任务。例如:
ExecutorService pool = Executors.newFixedThreadPool(1); // 线程池只有1个核心线程
for (int i = 0; i < 100; i++) {pool.execute(() -> { // 任务逻辑:存储大对象到 ThreadLocal});
}
该线程会执行 100 次任务,但线程本身不会被销毁(除非线程池关闭)。
2. 任务中存储大对象且未清理
假设每个任务向 ThreadLocal
中存储一个 10MB 的大对象(如 byte[10 * 1024 * 1024]
),但未调用 remove()
:
pool.execute(() -> {try {byte[] bigData = new byte[10 * 1024 * 1024]; // 10MB 大对象threadLocal.set(bigData); // 存储到当前线程的 ThreadLocalMap 中// 任务结束,但未调用 threadLocal.remove()} catch (Exception e) {e.printStackTrace();}
});
此时:
- 线程存活(线程池复用),
Thread
对象的threadLocals
不会被清空。 ThreadLocalMap
中的 Entry 因未调用remove()
,Key 虽被回收(变为null
),但 Value(10MB 数组)仍被强引用,无法回收。
3. 无效 Entry 持续累积导致 OOM
每次任务执行后,ThreadLocalMap
中会新增一个无效 Entry(Key 为 null
,Value 为 10MB 数组)。由于线程存活,这些无效 Entry 不会被自动清理,最终导致:
ThreadLocalMap
的table
数组被大量无效 Entry 占据(例如 100 次任务后,数组中有 100 个无效 Entry)。- 内存占用持续增长(100 次任务后约 1GB),最终触发 OOM(
OutOfMemoryError
)。
四、源码级分析:OOM 触发的具体过程
通过 ThreadLocalMap
的核心方法源码,详细分析无效 Entry 如何累积并导致 OOM。
1. set()
方法:插入新 Entry 并触发清理
ThreadLocal.set(T value)
方法的源码(JDK 8)如下:
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = t.threadLocals; // 获取当前线程的 ThreadLocalMapif (map != null) {// 计算 Key 的哈希位置int i = key.threadLocalHashCode & (map.table.length - 1);// 遍历哈希位置,查找是否已存在当前 Keyfor (Entry e = map.table[i]; e != null; e = map.table[nextIndex(i, map.table.length)]) {ThreadLocal<?> k = e.get();if (k == key) { // Key 已存在:更新 Valuee.value = value;return;}if (k == null) { // 找到无效 Entry:替换并清理replaceStaleEntry(key, value, i);return;}}// 未找到现有 Key:插入新 Entrymap.table[i] = new Entry(key, value);int sz = ++map.size;// 检查是否需要扩容或清理(阈值是容量的 0.75 倍)if (!map.cleanSomeSlots(i, sz) && sz >= map.threshold) {map.rehash(); // 扩容并重新哈希}} else {// 首次设置:初始化 ThreadLocalMapcreateMap(t, value);}
}
- 关键逻辑:插入新 Entry 前,若发现无效 Entry(Key 为
null
),会调用replaceStaleEntry
替换该 Entry,但仅清理当前位置附近的无效条目,无法保证完全清理。 - 扩容机制:当
size >= threshold
(容量 * 0.75)时,触发rehash()
扩容(容量翻倍),但扩容前仅清理部分无效条目(cleanSomeSlots
),无法彻底解决内存泄漏。
2. get()
方法:获取值并触发清理
ThreadLocal.get()
方法的源码(JDK 8)如下:
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = t.threadLocals;if (map != null) {// 计算 Key 的哈希位置int i = key.threadLocalHashCode & (map.table.length - 1);// 查找 EntryEntry e = map.table[i];if (e != null && e.get() == key) { // Key 存在且未失效return (T)e.value;}// Key 不存在或失效:触发清理并递归查找return (T)expungeStaleEntry(map, i, null);}// 首次获取:初始化 ThreadLocalMap(返回默认值)return setInitialValue();
}
expungeStaleEntry
方法:清理指定位置的无效 Entry,并将后续的无效 Entry 也一并清理(通过expungeStaleEntries
遍历数组)。- 局限性:
get()
仅清理当前哈希位置附近的无效条目,若无效条目分散在数组中,无法全部清理。
3. remove()
方法:主动清理 Entry
ThreadLocal.remove()
方法的源码(JDK 8)如下:
public void remove() {ThreadLocalMap m = threadLocals;if (m != null && m.remove(this) != null) { // 调用 ThreadLocalMap 的 remove 方法m.remove(this); // 从 table 中删除当前 Key 对应的 Entry}
}
ThreadLocalMap.remove(ThreadLocal<?> key)
:遍历table
数组,找到 Key 对应的 Entry 并删除(将数组位置置为null
),释放 Value 的引用。- 重要性:
remove()
是唯一能彻底清理无效 Entry 的方法,若未调用,Value 会一直被 Entry 强引用。
五、OOM 的触发条件总结
结合源码分析,ThreadLocal 导致 OOM 的核心条件如下:
条件 | 描述 |
---|---|
线程长期存活 | 线程池中的线程不复用(如 FixedThreadPool ),或线程未随任务结束而销毁。 |
存储大对象 | 任务中向 ThreadLocal 存储大对象(如大数组、大集合),且未及时清理。 |
未主动调用 remove() | 开发者未在任务结束时调用 ThreadLocal.remove() ,导致无效 Entry 持续累积。 |
清理机制未触发 | 线程存活期间未调用 get() 、set() 等方法,导致惰性清理未生效,无效 Entry 无法被回收。 |
六、避免 OOM 的最佳实践
基于源码和场景分析,避免 ThreadLocal 导致 OOM 的关键是 及时清理无效 Entry,具体措施如下:
1. 显式调用 remove()
清理
在任务的 finally
块中调用 ThreadLocal.remove()
,确保无论任务是否异常,都能清理当前线程的 ThreadLocal
数据:
ExecutorService pool = Executors.newFixedThreadPool(1);
for (int i = 0; i < 100; i++) {pool.execute(() -> {try {byte[] bigData = new byte[10 * 1024 * 1024]; // 10MB 大对象threadLocal.set(bigData);} finally {threadLocal.remove(); // 关键:清理当前线程的 ThreadLocal 数据}});
}
2. 避免存储大对象
尽量不在 ThreadLocal
中存储大对象(如大数组、大集合)。若必须存储,需评估对象生命周期,确保及时清理。
3. 合理选择线程池类型
- 对于短期任务(如 HTTP 请求处理),使用
CachedThreadPool
(线程动态创建/销毁),避免线程长期存活。 - 对于长期任务(如定时任务),使用
FixedThreadPool
但严格清理ThreadLocal
数据。
4. 监控与调优
通过内存分析工具(如 JProfiler、Arthas)监控 ThreadLocalMap
的内存占用,定位未清理的无效 Entry。
总结
ThreadLocal 导致 OOM 的根本原因是:线程池的线程长期存活,且任务中向 ThreadLocal 存储了大对象但未及时清理,导致 ThreadLocalMap 中的无效 Entry 持续累积,最终耗尽内存。
关键结论:
ThreadLocalMap
的 Entry 设计(弱引用 Key)无法自动回收 Value,必须依赖主动清理(remove()
)。- 线程池的线程复用特性会放大内存泄漏问题,需特别注意清理。
- 显式调用
remove()
是避免 OOM 的最有效手段。
🔴🟠🟡🟢🔵🟣🔴🔴🟠🟡🟢🔵🟣🔴🔴🟠🟡🟢🔵🟣🔴🔴🟠🟡🟢🔵🟣🔴
synchronized、volatile区别
synchronized 与 volatile 的深度解析(结合 JMM 与底层原理)
在 Java 并发编程中,synchronized
和 volatile
是最常用的同步机制,但它们的设计目标、实现原理和应用场景有本质区别。本文将从 JMM(Java 内存模型) 出发,结合底层内存交互、指令重排、锁优化等核心机制,系统对比两者的差异,并通过代码示例和场景分析说明其适用场景。
一、JMM 基础:并发问题的底层根源
Java 内存模型(JMM)是 Java 虚拟机规范中对内存交互的抽象定义,它通过 主内存(Main Memory) 和 线程本地内存(Working Memory) 的交互规则,解决了多线程环境下的 可见性、原子性、有序性 三大并发问题。
1. 主内存与线程本地内存
- 主内存:所有线程共享的内存区域,存储实例变量、静态变量等共享数据(对应物理内存的一部分)。
- 线程本地内存:每个线程私有的内存区域,存储主内存中变量的副本(缓存)。线程通过“读取主内存→本地计算→写回主内存”的流程操作共享变量。
2. JMM 的三大并发问题
问题 | 描述 |
---|---|
可见性 | 线程 A 修改了主内存中的变量,但线程 B 因本地缓存未刷新,无法立即看到最新值(如 flag 变量的延迟更新)。 |
原子性 | 多线程并发修改共享变量时,操作可能被中断(如 i++ 分解为“读取→修改→写入”三步),导致数据不一致。 |
有序性 | 编译器或处理器可能对指令重排序(优化性能),但单线程内重排序不影响结果(as-if-serial),多线程可能因重排序导致逻辑错误。 |
二、synchronized:互斥锁与内存屏障的深度实现
synchronized
是 Java 的内置锁机制,通过 监视器锁(Monitor) 实现互斥访问,其核心作用是保证 临界区(Lock 包裹的代码块) 的原子性、可见性和有序性。
1. 实现原理:锁的获取与释放
synchronized
的底层实现依赖 JVM 的 监视器锁(Monitor) 和操作系统的 互斥量(Mutex),核心流程如下:
(1) 加锁过程
- 偏向锁(优化):首次获取锁时,JVM 会记录线程 ID(偏向该线程),后续该线程再次获取锁时无需原子操作(无竞争时性能极高)。
- 轻量级锁(优化):若偏向锁被其他线程抢占,JVM 会通过 CAS(Compare-And-Swap)尝试获取锁,避免直接升级为重量级锁。
- 重量级锁(最终手段):若 CAS 失败,线程会进入内核态,通过操作系统互斥量(Mutex)阻塞等待,直到锁释放。
(2) 释放过程
- 线程执行完临界区代码后,释放 Monitor 锁。
- 内存屏障:释放锁前,JVM 会插入
StoreStore
屏障(禁止普通写与volatile
写重排),并强制将本地内存的修改刷新到主内存(保证可见性)。 - 线程释放锁后,其他线程竞争获取锁,获取前会插入
LoadLoad
屏障(禁止volatile
读与后续读重排),并从主内存加载最新值(保证可见性)。
2. 对 JMM 三大特性的支持
特性 | 具体实现 |
---|---|
可见性 | 锁释放时强制刷新本地内存到主内存;锁获取时强制从主内存加载最新值(通过 StoreStore 和 LoadLoad 屏障)。 |
原子性 | 临界区代码同一时间仅一个线程执行(互斥),保证复合操作(如 i++ )的原子性。 |
有序性 | 通过 happens-before 规则(锁的释放与获取存在偏序关系),禁止跨锁的指令重排(如临界区内的代码不会被重排到锁外)。 |
3. 应用场景
- 临界区保护:多线程修改共享变量(如计数器
count++
、状态标志isRunning
)。 - 方法同步:通过
synchronized
修饰方法(锁是当前对象或类,如public synchronized void method()
)。 - 单例模式(DCL):防止多线程重复实例化(需配合
volatile
避免指令重排)。
三、volatile:轻量级可见性与禁止重排的底层机制
volatile
是轻量级的同步机制,仅作用于 变量级别,核心作用是保证变量的 可见性 和 禁止指令重排,但不保证原子性。
1. 实现原理:主内存直连与内存屏障
volatile
的底层实现依赖 JVM 的 内存屏障(Memory Barrier),通过强制变量与主内存直接交互,避免线程本地缓存的延迟更新。
(1) 读取过程
- 线程读取
volatile
变量时,直接从主内存获取最新值(跳过本地缓存)。 - JVM 插入
LoadLoad
屏障(禁止volatile
读与后续普通读重排)和LoadStore
屏障(禁止volatile
读与后续普通写重排)。
(2) 写入过程
- 线程写入
volatile
变量时,立即将值刷新到主内存(不等待本地缓存同步)。 - JVM 插入
StoreStore
屏障(禁止普通写与volatile
写重排)和StoreLoad
屏障(禁止volatile
写与后续普通读重排)。
(3) 禁止指令重排
通过内存屏障,volatile
变量的读写操作会被限制在特定的顺序内,确保多线程下的逻辑正确性。例如:
// 以下两行代码不会被重排为 "b = 2; a = 1;"(若 a 是 volatile)
a = 1;
b = 2;
2. 对 JMM 三大特性的支持
特性 | 具体表现 |
---|---|
可见性 | 强制从主内存读取和写入,保证线程间变量值的实时同步(无本地缓存延迟)。 |
原子性 | 仅保证单次读/写操作的原子性(如 int a = 1 或 a = 1 ),但复合操作(如 a++ )不保证(仍需 synchronized )。 |
有序性 | 禁止编译器和处理器对 volatile 变量的指令重排(通过内存屏障实现)。 |
3. 应用场景
- 状态标志:单线程修改、多线程读取的布尔型变量(如
isRunning
、isShutdown
)。 - 单例模式(DCL):防止指令重排导致的空指针异常(需配合
synchronized
保证原子性)。 - 轻量级通知:配合
wait/notify
实现线程间协作(但需结合synchronized
使用)。
四、核心区别对比(表格+代码示例)
维度 | synchronized | volatile |
---|---|---|
作用范围 | 变量、方法、类(锁对象) | 仅变量 |
可见性 | 保证(锁释放刷主内存,锁获取读主内存) | 保证(主内存直连) |
原子性 | 保证(临界区互斥) | 不保证(仅单次读/写原子) |
有序性 | 保证(happens-before 规则) | 保证(禁止指令重排) |
线程阻塞 | 可能阻塞(多线程争抢锁时,进入内核态等待) | 不阻塞(无锁机制,始终在用户态执行) |
性能开销 | 较高(锁竞争、上下文切换,优化后轻量级锁开销低) | 较低(无锁,仅内存屏障) |
适用场景 | 复合操作、临界区保护(如计数器、状态更新) | 状态标志、单次读写、DCL |
代码示例 1:synchronized 保证原子性
public class SyncExample {private int count = 0;// synchronized 保证 count++ 的原子性public synchronized void increment() {count++; // 等价于 count = count + 1(读取→修改→写入三步)}public int getCount() {return count;}
}// 多线程测试:10 个线程各执行 1000 次 increment,最终 count 应为 10000
public static void main(String[] args) throws InterruptedException {SyncExample example = new SyncExample();ExecutorService pool = Executors.newFixedThreadPool(10);for (int i = 0; i < 10; i++) {pool.execute(() -> {for (int j = 0; j < 1000; j++) {example.increment();}});}pool.shutdown();pool.awaitTermination(1, TimeUnit.MINUTES);System.out.println(example.getCount()); // 输出 10000(正确)
}
- 分析:
synchronized
保证increment()
方法的互斥执行,避免了多线程并发修改导致的计数错误。
代码示例 2:volatile 保证可见性但不保证原子性
public class VolatileExample {private volatile int count = 0; // volatile 保证可见性,但不保证原子性public void increment() {count++; // 非原子操作(读取→修改→写入)}public int getCount() {return count;}
}// 多线程测试:10 个线程各执行 1000 次 increment,最终 count 可能小于 10000
public static void main(String[] args) throws InterruptedException {VolatileExample example = new VolatileExample();ExecutorService pool = Executors.newFixedThreadPool(10);for (int i = 0; i < 10; i++) {pool.execute(() -> {for (int j = 0; j < 1000; j++) {example.increment();}});}pool.shutdown();pool.awaitTermination(1, TimeUnit.MINUTES);System.out.println(example.getCount()); // 输出可能小于 10000(错误)
}
- 分析:
volatile
保证了count
的可见性(线程能立即看到最新值),但count++
是复合操作(非原子),多线程并发时仍可能丢失更新。
代码示例 3:volatile 禁止指令重排(DCL 单例模式)
public class Singleton {// volatile 禁止指令重排,防止多线程获取到未初始化的对象private static volatile Singleton instance;private Singleton() {}// 双重检查锁定(DCL)public static Singleton getInstance() {if (instance == null) { // 第一次检查(无锁)synchronized (Singleton.class) { // 加锁if (instance == null) { // 第二次检查(防竞争)instance = new Singleton(); // 关键:禁止重排}}}return instance;}
}
- 分析:
instance = new Singleton()
底层会分解为:- 分配内存空间;
- 初始化对象;
- 将内存地址赋值给
instance
(指针指向对象)。
若未使用volatile
,编译器可能重排为“1→3→2”,导致其他线程获取到未初始化的对象(instance
不为null
,但对象未初始化)。volatile
通过内存屏障禁止此重排。
五、典型误区与澄清
误区 1:volatile 可以替代 synchronized
- 错误:
volatile
无法保证原子性,无法替代synchronized
处理复合操作(如i++
)。 - 正确:
volatile
仅适用于单次读写场景(如状态标志),复合操作需配合synchronized
或使用AtomicXXX
类(基于 CAS 保证原子性)。
误区 2:synchronized 性能一定比 volatile 差
- 错误:JVM 对无竞争的
synchronized
优化为 偏向锁、轻量级锁(用户态 CAS 操作,无内核态切换),性能接近volatile
。仅在锁竞争激烈时升级为重量级锁(内核态阻塞),性能下降。
误区 3:指令重排对单线程无影响
- 正确:单线程内指令重排遵循
as-if-serial
规则(单线程执行结果与顺序执行一致),但多线程可能因重排导致逻辑错误(如 DCL 未加volatile
)。
六、总结
- synchronized 是“重量级”同步机制,通过锁保证临界区的原子性、可见性和有序性,适用于复合操作或需要互斥的场景。
- volatile 是“轻量级”同步机制,通过主内存直连保证可见性和禁止指令重排,但不保证原子性,适用于状态标志、单次读写等简单场景。
选择原则:
- 若需保证原子性(如计数器、状态更新),用
synchronized
或AtomicXXX
。 - 若仅需保证可见性(如状态标志),用
volatile
。 - 复合操作(如
i++
)需结合两者(如 DCL 中volatile
配合synchronized
)。
🔴🟠🟡🟢🔵🟣🔴🔴🟠🟡🟢🔵🟣🔴🔴🟠🟡🟢🔵🟣🔴🔴🟠🟡🟢🔵🟣🔴
synchronized锁粒度、模拟死锁场景
一、synchronized 锁粒度详解
synchronized 是 Java 中最经典的同步机制,其核心是通过 监视器锁(Monitor Lock) 实现对共享资源的互斥访问。锁的粒度(即锁的作用范围)决定了哪些操作会被同步,主要分为 对象锁 和 类锁 两种形式。
1. 对象锁(Instance Lock)
对象锁作用于 类的实例对象,确保同一时间只有一个线程能访问该对象的同步代码块或同步方法。其核心是:每个 Java 对象都与一个内置的监视器(Monitor)绑定,线程进入同步代码块前需获取该对象的 Monitor,退出时释放。
(1)使用方式
-
同步代码块:显式指定锁对象(通常是
this
或其他实例)。public class ObjectLockDemo {private final Object lock = new Object(); // 专用锁对象(推荐)public void syncBlock() {synchronized (lock) { // 锁是 lock 对象// 临界区代码}}public synchronized void syncMethod() { // 锁是当前对象(this)// 临界区代码} }
-
同步方法:默认锁是当前对象(实例方法)或类的 Class 对象(静态方法,见下文类锁)。
(2)关键特性
- 锁的独立性:不同实例对象的锁相互独立。例如,两个不同的
ObjectLockDemo
实例的syncMethod()
可以被不同线程同时执行。 - 可重入性:同一线程可多次获取同一对象的锁(计数器递增),避免自身死锁。例如,递归调用同步方法时不会阻塞自己。
- 锁的释放:锁在同步代码块执行完毕或发生异常时自动释放(通过
monitorexit
指令)。
2. 类锁(Class Lock)
类锁作用于 类的 Class 对象(每个类在 JVM 中仅有一个 Class 对象),确保同一时间只有一个线程能访问该类的所有同步静态方法或同步代码块(使用 类名.class
作为锁)。
(1)使用方式
-
同步静态方法:默认锁是类的 Class 对象。
public class ClassLockDemo {public static synchronized void staticSyncMethod() {// 临界区代码(锁是 ClassLockDemo.class)} }
-
同步代码块(显式指定类锁):
public class ClassLockDemo {public void syncClassBlock() {synchronized (ClassLockDemo.class) { // 锁是类的 Class 对象// 临界区代码}} }
(2)关键特性
- 全局唯一性:类锁是类级别的,所有实例共享同一把锁。例如,无论创建多少个
ClassLockDemo
实例,调用staticSyncMethod()
都会被同步。 - 与对象锁互斥:类锁和对象锁是独立的。例如,线程 A 持有对象锁时,线程 B 仍可获取类锁(反之亦然)。
3. 锁粒度的选择
- 对象锁:适用于保护实例级别的共享资源(如实例变量)。
- 类锁:适用于保护静态变量或全局共享资源(如单例模式中的实例创建)。
二、synchronized 的三大性质
1. 原子性(Atomicity)
原子性指一个操作或多个操作不可中断,要么全部执行完成,要么全部不执行。
(1)synchronized 如何保证原子性?
synchronized 的底层通过 JVM 的 monitorenter
和 monitorexit
指令实现:
- monitorenter:线程尝试获取对象的 Monitor。若 Monitor 未被锁定(计数器为 0),则获取锁并将计数器置为 1;若已被当前线程持有(计数器 > 0),则计数器递增。
- monitorexit:线程释放锁,计数器递减。若计数器归零,则释放 Monitor。
这一过程保证了临界区代码的原子性,因为其他线程无法中断当前线程对 Monitor 的持有。
(2)对比其他操作的原子性
- 基本类型变量:
int a = 10
是原子操作(JVM 保证);但a++
(读取-修改-写入)不是原子操作。 - long/double:在 32 位 JVM 上,
long
和double
的读写可能被拆分为两次 32 位操作(非原子),但 JVM 允许通过-XX:+UseCompressedOops
等参数优化。 - synchronized 的原子性范围:覆盖整个同步代码块,无论内部有多少操作。
2. 可见性(Visibility)
可见性指一个线程对共享变量的修改,其他线程能立即感知。
(1)synchronized 如何保证可见性?
- 释放锁时刷新主内存:线程退出同步代码块(执行
monitorexit
)前,会将所有修改的共享变量从工作内存刷新到主内存。 - 获取锁时重载主内存:线程进入同步代码块(执行
monitorenter
)前,会从主内存重新加载所有共享变量到工作内存,确保看到最新值。
这一机制通过 JVM 的内存屏障(Memory Barrier)实现,强制线程与主内存的同步。
(2)对比 volatile 的可见性
volatile
仅保证单个变量的可见性,且通过lock
指令实现(与 synchronized 类似,但无锁的获取/释放)。synchronized
保证临界区内所有变量的可见性,且能处理多个变量的复合操作(如i++
)。
3. 有序性(Ordering)
有序性指程序的执行顺序与代码编写的顺序一致(单线程内有序,多线程内可能重排序)。
(1)synchronized 如何保证有序性?
synchronized 通过 禁止编译器/CPU 对同步代码块内的指令重排序 来保证有序性。具体通过内存屏障实现:
- 在同步代码块的入口(
monitorenter
)插入 写屏障(StoreStore Barrier),禁止普通写与同步块的写重排序。 - 在同步代码块的出口(
monitorexit
)插入 读屏障(LoadLoad Barrier),禁止同步块的读与普通读重排序。
(2)典型案例:双重检查锁定(DCL)
单例模式中,若不使用 volatile
修饰实例变量,可能因指令重排序导致线程获取未初始化的对象:
public class Singleton {private static Singleton instance; // 未加 volatile 时可能重排序public static Singleton getInstance() {if (instance == null) { // 第一次检查synchronized (Singleton.class) {if (instance == null) { // 第二次检查instance = new Singleton(); // 可能重排序为:分配内存 → 指向地址 → 初始化对象}}}return instance;}
}
- 问题:
instance = new Singleton()
实际分为三步:- 分配内存空间;
- 初始化对象;
- 将内存地址赋值给
instance
(让引用指向对象)。
若步骤 2 和 3 重排序,线程 B 可能在instance
不为空时(已指向地址但未初始化)直接使用,导致错误。
- 解决:用
volatile
修饰instance
,禁止步骤 2 和 3 的重排序。但 synchronized 本身也能通过内存屏障禁止重排序,因此在同步块内的操作是有序的。
三、死锁的场景模拟与分析
死锁(Deadlock)指两个或多个线程互相持有对方需要的锁,且无法继续执行的状态。
1. 死锁的四个必要条件
- 互斥条件:锁一次只能被一个线程持有。
- 持有并等待:线程持有至少一个锁,并等待获取其他线程持有的锁。
- 不可抢占:锁只能被持有者主动释放,不能被其他线程强行抢占。
- 循环等待:线程间形成环状等待链(线程 A 等待线程 B 的锁,线程 B 等待线程 A 的锁)。
2. 死锁代码示例
以下代码构造了一个典型的死锁场景:
// 类 E 和 E1 互相持有对方的锁
class E {public static synchronized void methodE() throws InterruptedException {System.out.println(Thread.currentThread().getName() + " 进入 E.methodE");Thread.sleep(1000); // 模拟业务操作E1.methodE1(); // 请求 E1 的类锁(静态方法,锁是 E1.class)}
}class E1 {public static synchronized void methodE1() throws InterruptedException {System.out.println(Thread.currentThread().getName() + " 进入 E1.methodE1");Thread.sleep(1000); // 模拟业务操作E.methodE(); // 请求 E 的类锁(静态方法,锁是 E.class)}
}public class DeadLockDemo {public static void main(String[] args) {// 线程 1:先获取 E 的类锁,再请求 E1 的类锁new Thread(() -> {try {E.methodE();} catch (InterruptedException e) {e.printStackTrace();}}, "Thread-1").start();// 线程 2:先获取 E1 的类锁,再请求 E 的类锁new Thread(() -> {try {E1.methodE1();} catch (InterruptedException e) {e.printStackTrace();}}, "Thread-2").start();}
}
3. 死锁现象
运行代码后,输出如下(程序卡住,无后续输出):
Thread-1 进入 E.methodE
Thread-2 进入 E1.methodE1
此时,线程 1 持有 E.class
锁并等待 E1.class
锁,线程 2 持有 E1.class
锁并等待 E.class
锁,形成循环等待。
4. 死锁的检测与避免
(1)检测死锁
- 工具检测:使用 JDK 自带的
jconsole
、jvisualvm
或jstack
工具查看线程状态。例如,jstack <PID>
会输出线程的堆栈信息,其中包含锁的持有和等待关系。 - 日志分析:在代码中添加日志,记录锁的获取和释放顺序,定位可能的循环等待。
(2)避免死锁的方法
- 固定加锁顺序:所有线程按相同的顺序获取锁。例如,线程 1 和线程 2 都先获取
E.class
锁,再获取E1.class
锁。 - 使用超时机制:通过
Lock.tryLock(long timeout, TimeUnit unit)
替代synchronized
,设置超时时间,避免无限等待。 - 减少锁的嵌套:简化同步逻辑,避免多个锁的嵌套使用。
- 锁分离:使用读写锁(
ReentrantReadWriteLock
),分离读锁和写锁,减少竞争。
四、总结
特性 | synchronized | volatile |
---|---|---|
原子性 | 保证同步代码块/方法的原子性(通过 Monitor 锁)。 | 仅保证基本类型(除 long/double)和引用类型的读/写原子性(依赖 JVM 实现)。 |
可见性 | 释放锁时刷新主内存,获取锁时重载主内存(通过内存屏障)。 | 强制变量从主内存读取/写入(通过 lock 指令)。 |
有序性 | 禁止同步代码块内的指令重排序(通过内存屏障)。 | 禁止指令重排序(通过 happens-before 规则)。 |
适用场景 | 复杂临界区(多步操作、多变量共享)。 | 单一变量的可见性需求(如状态标志)。 |
最佳实践:
- 优先使用
volatile
解决可见性问题(简单高效)。 - 复杂同步逻辑使用
synchronized
,并尽量缩小锁的范围(如使用专用锁对象)。 - 避免嵌套锁,若必须使用则固定加锁顺序。
- 死锁发生时,通过工具(如
jstack
)分析线程状态,定位循环等待链。
🔴🟠🟡🟢🔵🟣🔴🔴🟠🟡🟢🔵🟣🔴🔴🟠🟡🟢🔵🟣🔴🔴🟠🟡🟢🔵🟣🔴
Java并发和并行
Java 并发与并行:核心概念、区别与实践
在 Java 编程中,并发(Concurrency) 和 并行(Parallelism) 是两个核心概念,用于描述多任务的处理方式。它们既有联系又有本质区别,理解两者的差异对设计高效的并发程序至关重要。
一、核心定义
1. 并发(Concurrency)
定义:多个任务在 同一时间间隔 内交替执行,宏观上看起来“同时发生”,但微观上同一时刻只有一个任务在执行(单处理器环境)。
本质:通过 任务切换 实现“伪同时”,核心是解决任务的 调度与协调。
示例:
单核 CPU 上运行一个 Web 服务器,同时处理 10 个用户的请求。CPU 会在 10 个请求之间快速切换(时间片轮转),每个请求的响应看似“同时”完成,但实际是逐个处理的。
2. 并行(Parallelism)
定义:多个任务在 同一时刻 同时执行,依赖 多处理器/多核心 硬件支持。
本质:通过 物理资源的多线程执行 实现真正的“同时”,核心是利用多核的计算能力。
示例:
8 核 CPU 上运行 8 个线程,每个线程独占一个核心,同时处理不同的计算任务(如大数据并行排序)。
二、关键区别
维度 | 并发(Concurrency) | 并行(Parallelism) |
---|---|---|
核心目标 | 解决任务的 调度与协调(如何高效切换任务) | 解决任务的 加速执行(如何利用多核资源) |
硬件依赖 | 单处理器即可实现(依赖时间片轮转) | 必须依赖多处理器/多核心 |
执行方式 | 微观上单任务逐个执行(交替运行) | 微观上多任务同时执行(物理并行) |
典型场景 | IO 密集型任务(如 Web 服务器、数据库连接池) | CPU 密集型任务(如数值计算、图像渲染) |
三、Java 中的实现方式
1. 并发的实现:多线程与任务调度
Java 通过 多线程(Thread) 实现并发,核心机制是 操作系统的线程调度(时间片轮转)。即使只有单核 CPU,Java 也能通过线程切换模拟“同时执行”。
关键工具:
Thread
类:直接创建线程(new Thread().start()
)。Runnable
/Callable
接口:定义任务逻辑(Runnable
无返回值,Callable
有返回值)。ExecutorService
线程池:管理线程生命周期,避免频繁创建/销毁线程的开销(如Executors.newFixedThreadPool(5)
)。
示例:单核下的并发(任务切换)
public class ConcurrencyDemo {public static void main(String[] args) {// 创建两个任务Runnable task1 = () -> {for (int i = 0; i < 5; i++) {System.out.println("Task1: " + i);try { Thread.sleep(100); } catch (InterruptedException e) {}}};Runnable task2 = () -> {for (int i = 0; i < 5; i++) {System.out.println("Task2: " + i);try { Thread.sleep(100); } catch (InterruptedException e) {}}};// 单线程依次执行(非并发)// task1.run();// task2.run();// 多线程并发执行(单核下交替运行)new Thread(task1).start();new Thread(task2).start();}
}
输出说明:
单核环境下,两个线程的输出会交替出现(如 Task1:0
→ Task2:0
→ Task1:1
→ Task2:1
…),宏观上“同时”执行,微观上是 CPU 快速切换。
2. 并行的实现:多核与多线程
Java 利用 多核 CPU 实现并行,通过 Fork/Join
框架、并行流(Parallel Streams
)或直接创建多线程(线程数 ≤ 核心数)实现任务的真正同时执行。
关键工具:
ForkJoinPool
:分治任务框架(如RecursiveTask
递归拆分任务)。- 并行流(
stream().parallel()
):自动将任务分配到多核执行(底层基于ForkJoinPool
)。 - 直接创建多线程(线程数等于核心数):每个线程绑定一个核心,避免上下文切换开销。
示例:多核下的并行(同时执行)
import java.util.stream.IntStream;public class ParallelismDemo {public static void main(String[] args) {// 并行流:自动利用多核执行IntStream.range(0, 5).parallel() // 开启并行.forEach(i -> {System.out.println("Parallel Task: " + i + " on Thread: " + Thread.currentThread().getName());try { Thread.sleep(100); } catch (InterruptedException e) {}});}
}
输出说明:
多核环境下,多个线程的输出会同时出现(如 Parallel Task:0
和 Parallel Task:1
可能同时打印),说明任务在多个核心上同时执行。
四、并发与并行的联系
- 并行是并发的扩展:当并发的任务数超过 CPU 核心数时,系统会将部分任务分配到不同核心并行执行。例如,8 核 CPU 上运行 16 个线程,其中 8 个线程并行执行,另外 8 个线程并发等待。
- 并发是并行的基础:并行需要先通过并发机制(如线程调度)将任务分配到不同核心,才能实现真正的同时执行。
五、挑战与注意事项
1. 并发的挑战
- 线程安全:多个线程共享资源时可能出现竞态条件(Race Condition),需通过
synchronized
、Lock
或原子类(AtomicInteger
)保证原子性。 - 上下文切换开销:线程切换需要保存/恢复寄存器状态,过多线程会导致性能下降(需控制线程数,如线程池大小)。
- 死锁/活锁:线程间互相等待锁时可能导致死锁(需通过固定加锁顺序、超时机制避免)。
2. 并行的挑战
- 任务划分:需将大任务拆分为独立子任务(如分治算法),避免任务间的依赖(否则无法并行)。
- 负载均衡:子任务计算量需尽量均衡,避免某些核心空闲(如
ForkJoinPool
的工作窃取机制)。 - 资源竞争:多核同时访问共享资源时仍需同步(如并行流中修改共享变量需谨慎)。
六、总结
维度 | 并发(Concurrency) | 并行(Parallelism) |
---|---|---|
核心 | 任务交替执行(单核模拟“同时”) | 任务真正同时执行(多核物理并行) |
Java 实现 | 多线程、线程池(ExecutorService ) | 并行流、ForkJoinPool 、多线程(线程数=核心数) |
适用场景 | IO 密集型(如 Web 服务器、数据库交互) | CPU 密集型(如数值计算、大数据处理) |
关键问题 | 线程安全、上下文切换、死锁 | 任务划分、负载均衡、资源竞争 |
最佳实践:
- IO 密集型任务优先用并发(减少线程等待时间)。
- CPU 密集型任务优先用并行(充分利用多核性能)。
- 避免过度设计:单核环境下无需强行并行,并发调度已足够高效。
🔴🟠🟡🟢🔵🟣🔴🔴🟠🟡🟢🔵🟣🔴🔴🟠🟡🟢🔵🟣🔴🔴🟠🟡🟢🔵🟣🔴
怎么提高并发量,请列举你所知道的方案?
要系统性地提高系统的并发处理能力,需从 资源效率、请求链路优化、架构扩展性 三个核心维度展开,每个维度包含多个技术点,且需结合具体业务场景选择组合方案。以下是更深入的技术细节和落地实践:
一、静态资源优化:降低动态请求压力
静态资源(HTML、CSS、JS、图片、视频)的优化是提升并发的“低门槛高收益”手段,核心目标是 减少服务器计算、降低网络带宽消耗。
1. HTML 静态化:从动态生成到预渲染
- 原理:将动态渲染的页面(如用户个人中心、商品详情页)提前生成静态 HTML 文件,用户直接访问静态文件,避免服务器每次请求都执行数据库查询和模板渲染。
- 实现方式:
- CMS 系统自动生成:使用 WordPress、Drupal 等 CMS 系统,通过“发布”操作自动生成静态 HTML(如 WordPress 的
wp-content/cache
目录)。 - 定时任务生成:对低频更新页面(如首页、活动页),通过 Quartz 或 Linux Crontab 定时调用渲染接口生成静态文件(示例:每天凌晨 3 点生成首页
index.html
)。 - 动态转静态中间件:使用 Nginx 的
ngx_http_rewrite_module
或 OpenResty 的 Lua 脚本,将动态 URL(如/article/123
)映射到静态文件(/data/html/article_123.html
)。
- CMS 系统自动生成:使用 WordPress、Drupal 等 CMS 系统,通过“发布”操作自动生成静态 HTML(如 WordPress 的
- 效果:某新闻网站将首页从动态渲染改为静态化后,服务器 CPU 使用率从 80% 降至 30%,响应时间从 500ms 缩短至 50ms。
2. 图片/静态资源分离与 CDN 加速
- 图片服务器独立:
- 架构设计:主服务器(如 Nginx)仅返回图片 URL(如
https://img.example.com/photo.jpg
),用户直接访问独立图片服务器(如img.example.com
)或 CDN 节点。 - 性能优化:图片服务器关闭不必要的模块(如 Apache 的
mod_rewrite
),仅保留静态文件服务;使用sendfile
系统调用(Nginx 配置sendfile on;
)减少用户态到内核态的拷贝。
- 架构设计:主服务器(如 Nginx)仅返回图片 URL(如
- CDN 深度集成:
- 选型:根据业务需求选择云 CDN(如阿里云 CDN、Cloudflare)或专用 CDN(如 Akamai)。
- 配置步骤:
- 将静态资源(图片、JS、CSS)上传至 CDN 源站(如阿里云 OSS)。
- 在 CDN 控制台配置缓存规则(如图片缓存 30 天,JS 缓存 7 天)。
- 开启智能压缩(如 Brotli 压缩,压缩率可达 20%~30%)。
- 配置回源策略(如优先从源站拉取,缓存过期后异步更新)。
- 效果:某电商网站使用 CDN 后,图片加载时间从 800ms 降至 200ms,源站带宽成本降低 60%。
3. 静态资源缓存策略:多层防护
-
浏览器缓存:通过 HTTP 头控制缓存行为(示例 Nginx 配置):
location /static/ {expires 30d; # 静态资源缓存 30 天add_header Cache-Control "public, max-age=2592000"; }
-
反向代理缓存(Nginx):对未命中的静态资源回源到源站,并缓存到本地(示例):
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=my_cache:10m inactive=60m; server {location /static/ {proxy_pass http://source_server;proxy_cache my_cache;proxy_cache_valid 200 30d; # 200 响应缓存 30 天} }
-
CDN 缓存:CDN 节点缓存静态资源,通过
Cache-Control
或stale-while-revalidate
策略平衡实时性与性能(如 Cloudflare 的“Cache Everything”规则)。
二、动态请求处理:提升服务器吞吐量
动态请求(如用户登录、下单、查询数据库)需服务器实时计算,优化方向包括 负载均衡、应用服务器调优、异步化。
1. 负载均衡:分散流量的核心枢纽
-
硬件负载均衡(F5/A10):
- 原理:基于四层(TCP/UDP)或七层(HTTP)协议,将请求按算法(轮询、加权轮询、IP 哈希)分配到后端服务器。
- 适用场景:超大规模流量(如单集群 10 万+ QPS),需硬件级性能保障(F5 最大可处理 200Gbps 流量)。
- 配置示例:在 F5 中配置虚拟服务器(VIP)指向后端应用服务器集群,设置健康检查(如 HTTP 200 响应)自动剔除故障节点。
-
软件负载均衡(LVS/Nginx):
-
LVS(四层):基于内核模块
ip_vs
实现,支持 NAT、DR、TUN 模式(示例 DR 模式配置):# LVS 主节点配置(/etc/sysconfig/ipvsadm) IPVSADM='/sbin/ipvsadm' $IPVSADM -A -t 192.168.1.100:80 -s rr # 添加虚拟服务,轮询算法 $IPVSADM -a -t 192.168.1.100:80 -r 192.168.1.101:80 -m # 添加后端节点 1 $IPVSADM -a -t 192.168.1.100:80 -r 192.168.1.102:80 -m # 添加后端节点 2
-
Nginx(七层):基于 HTTP 协议,支持更灵活的路由规则(如按 URL 路径、Cookie 分发):
http {upstream app_servers {server 192.168.1.101:8080 weight=3; # 权重 3,承担 3/4 流量server 192.168.1.102:8080 weight=1;}server {listen 80;location / {proxy_pass http://app_servers;}} }
-
-
云厂商负载均衡(阿里云 SLB/AWS ALB):
- 优势:集成健康检查(如 TCP 检查、HTTP 检查)、自动扩缩容(根据 CPU 使用率自动增减后端实例)、SSL 卸载(减少后端服务器加密开销)。
- 适用场景:云原生架构,无需自建负载均衡集群。
2. 应用服务器优化:提升单节点处理能力
-
线程池调优:
-
Tomcat 线程池参数(
server.xml
):<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"maxThreads="200" # 最大工作线程数(建议为 CPU 核心数×2)minSpareThreads="50" # 最小空闲线程数(提前创建)acceptCount="100" # 请求队列长度(超出则拒绝)connectionTimeout="20000"/> # 连接超时时间(ms)
- 经验值:CPU 密集型应用(如计算服务)
maxThreads
设为 CPU 核心数×1;IO 密集型应用(如数据库查询)设为 CPU 核心数×2~4。
- 经验值:CPU 密集型应用(如计算服务)
-
Jetty 线程池:通过
qtp-*
线程池控制,建议配置maxThreads=200
,acceptors=4
(与 CPU 核心数相关)。
-
-
异步处理:释放主线程
-
Servlet 3.0 异步支持:通过
AsyncContext
将耗时操作转移到后台线程(示例):@WebServlet(urlPatterns = "/async", asyncSupported = true) public class AsyncServlet extends HttpServlet {protected void doGet(HttpServletRequest req, HttpServletResponse resp) {AsyncContext asyncCtx = req.startAsync();asyncCtx.setTimeout(30000); // 超时时间 30sexecutor.submit(() -> {try {// 耗时操作(如调用外部 API)String result = callExternalAPI();asyncCtx.getResponse().setCharacterEncoding("UTF-8");asyncCtx.getResponse().getWriter().write(result);} finally {asyncCtx.complete();}});} }
-
Spring 异步(@Async):通过自定义线程池处理耗时方法(示例):
@Service public class OrderService {@Autowiredprivate OrderRepository orderRepo;@Async("orderExecutor") // 使用自定义线程池public CompletableFuture<Void> sendNotification(Long orderId) {Order order = orderRepo.findById(orderId).orElseThrow();smsClient.send(order.getUserPhone(), "订单已支付");return CompletableFuture.completedFuture(null);} }// 配置自定义线程池 @Configuration @EnableAsync public class AsyncConfig {@Bean("orderExecutor")public Executor orderExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(50); // 核心线程数executor.setMaxPoolSize(200); // 最大线程数executor.setQueueCapacity(1000);// 任务队列容量executor.setThreadNamePrefix("Order-Async-");executor.initialize();return executor;} }
-
-
无状态设计:应用服务器不存储会话(Session),通过 Redis 或 Memcached 集中管理(示例 Spring Session 配置):
# application.properties spring.session.store-type=redis spring.redis.host=redis.example.com spring.redis.port=6379
- 效果:支持水平扩展,新增应用服务器无需同步会话数据。
3. 异步化与消息队列:削峰填谷
-
消息队列选型:
- Kafka:高吞吐量(百万级 TPS),适合日志收集、大数据流处理(如 Flink 实时计算)。
- RocketMQ:支持事务消息(如订单支付与库存扣减的一致性),适合电商场景。
- RabbitMQ:支持多种消息模型(直连、广播),适合小规模异步任务(如通知推送)。
-
典型流程:
- 用户发起请求(如下单)。
- 应用服务器将核心操作(如扣减库存)写入消息队列。
- 主线程返回“下单成功”,释放连接。
- 消费者从队列拉取消息,执行耗时操作(如发送短信、更新物流)。
-
代码示例(Spring Kafka):
// 生产者:下单后发送消息 @Service public class OrderService {@Autowiredprivate KafkaTemplate<String, Order> orderKafkaTemplate;public void createOrder(Order order) {// 1. 扣减库存(同步)inventoryService.deductStock(order.getProductId());// 2. 写入数据库(同步)orderRepo.save(order);// 3. 发送异步消息(通知物流、积分)orderKafkaTemplate.send("order_topic", order);} }// 消费者:处理物流通知 @KafkaListener(topics = "order_topic", groupId = "logistics_group") public void handleLogistics(Order order) {logisticsService.sendNotification(order); // 耗时操作(如调用物流 API) }
三、数据库优化:解决单点瓶颈
数据库是大多数系统的性能瓶颈,优化方向包括 分库分表、读写分离、索引优化、缓存加速。
1. 分库分表:分散数据存储
-
垂直分库:
-
原理:按业务模块拆分数据库(如用户库
user_db
、订单库order_db
、商品库product_db
)。 -
实现方式:
-
应用层路由:代码中根据业务类型选择数据库(如
userMapper
连接user_db
)。 -
中间件代理:ShardingSphere 自动路由(示例配置):
# sharding-jdbc 配置(application.yml) spring:shardingsphere:datasources:ds0:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://mysql0.example.com:3306/user_dbds1:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://mysql1.example.com:3306/order_dbrules:database:sharding-column: user_idsharding-algorithm-name: db_inlinetables:order_table:actual-data-nodes: ds$->{0..1}.order_$->{0..1}database-strategy:standard:sharding-column: user_idsharding-algorithm-name: db_inlinetable-strategy:standard:sharding-column: order_idsharding-algorithm-name: table_inline
-
-
-
水平分表:
- 原理:将大表按规则(如用户 ID 取模、时间范围)拆分为多个子表(如
order_0
、order_1
)。 - 分片键选择:优先使用高频查询字段(如
user_id
),避免跨分片查询(如WHERE user_id=123
只访问order_123%10=3
的表)。 - 实现工具:ShardingSphere(Java)、DRDS(阿里云)、MyCat(开源)。
- 原理:将大表按规则(如用户 ID 取模、时间范围)拆分为多个子表(如
-
效果:某电商订单表单表数据量 5000 万行,QPS 8000,分表(10 张)后单表 500 万行,QPS 提升至 2 万。
2. 读写分离:分担主库压力
-
主从复制:
-
MySQL 主从复制:基于 Binlog 同步(示例配置):
# 主库 my.cnf server-id=1 log-bin=mysql-bin binlog-format=ROW# 从库 my.cnf server-id=2 relay-log=relay-bin read-only=1 # 从库只读
-
同步延迟监控:通过
SHOW SLAVE STATUS
查看Seconds_Behind_Master
(正常应 < 1s)。
-
-
中间件代理:
-
MyCat:配置读写分离规则(示例
schema.xml
):<schema name="order_schema"><table name="order_table" dataNode="dn1,dn2"/> </schema> <dataNode name="dn1" dataHost="master_host" database="order_db"/> <dataNode name="dn2" dataHost="slave_host" database="order_db_slave"/> <dataHost name="master_host" maxCon=100 minCon=10 balance="0"><writeHost host="master" url="mysql://master_user:password@master_ip:3306"/> </dataHost> <dataHost name="slave_host" maxCon=100 minCon=10 balance="1"><readHost host="slave" url="mysql://slave_user:password@slave_ip:3306"/> </dataHost>
balance="0"
:所有读请求到写库(主库);balance="1"
:读请求随机到从库。
-
-
应用层控制:在代码中区分读写操作(如 MyBatis 拦截器):
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class ReadWriteSplitInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {MappedStatement ms = (MappedStatement) invocation.getArgs()[0];String methodName = ms.getId();if (methodName.contains("select")) { // 查询操作路由到从库return invocation.proceedWithSlave();} else { // 写操作路由到主库return invocation.proceedWithMaster();}} }
3. 索引与 SQL 优化
-
索引设计原则:
- 覆盖索引:查询所需字段全部包含在索引中(如
SELECT id, name FROM user WHERE age=20
,创建(age, name)
索引)。 - 复合索引顺序:高频条件字段在前(如
WHERE user_id=? AND status=?
,索引(user_id, status)
比(status, user_id)
更高效)。 - 避免冗余索引:定期使用
SHOW INDEX FROM table
检查并删除重复索引。
- 覆盖索引:查询所需字段全部包含在索引中(如
-
慢 SQL 治理:
-
定位慢 SQL:开启 MySQL 慢查询日志(
slow_query_log=ON
,long_query_time=1
)。 -
分析执行计划:使用
EXPLAIN
查看索引是否命中、是否全表扫描(示例):EXPLAIN SELECT * FROM order_table WHERE user_id=123 AND create_time > '2024-01-01';
- 关注
type
(理想值为ref
或eq_ref
,避免ALL
全表扫描)、key
(实际使用的索引)。
- 关注
-
-
锁优化:
-
乐观锁:使用版本号(
version
字段)避免悲观锁(示例):UPDATE product SET stock=stock-1, version=version+1 WHERE id=123 AND version=old_version;
-
减少事务时长:将非必要操作移到事务外(如日志记录)。
-
四、缓存策略:减少重复计算与数据库访问
缓存是提升并发的核心手段,通过存储高频数据副本,避免重复计算或数据库查询。
1. 本地缓存(进程内缓存)
-
Guava Cache:
-
核心特性:基于 LRU 淘汰策略,支持容量限制、过期时间(
expireAfterAccess
/expireAfterWrite
)。 -
示例代码:
Cache<Long, User> userCache = CacheBuilder.newBuilder().maximumSize(1000) // 最大容量 1000.expireAfterAccess(30, TimeUnit.MINUTES) // 30 分钟无访问则过期.build();// 读取缓存(未命中则查数据库) User user = userCache.get(userId, () -> userDao.getUserById(userId));
-
-
Caffeine:
-
优势:比 Guava 更快(基于 W-TinyLFU 算法),支持基于权重的淘汰、刷新策略。
-
示例配置:
Cache<Long, User> userCache = Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(30, TimeUnit.MINUTES).refreshAfterWrite(5, TimeUnit.MINUTES) // 5 分钟后自动刷新.build(key -> userDao.getUserById(key));
-
2. 分布式缓存(跨进程缓存)
-
Redis 核心操作:
- 字符串(String):存储单个值(如用户会话
user:123:info
)。 - 哈希(Hash):存储对象(如
user:123
对应{name: "张三", age: 25}
)。 - 列表(List):存储队列(如消息队列
mq:order
)。 - 集合(Set):存储标签(如
tag:hot
存储热门商品 ID)。 - 有序集合(ZSet):存储排行榜(如
rank:sales
按销量排序)。
- 字符串(String):存储单个值(如用户会话
-
缓存穿透:
-
问题:查询不存在的数据(如
user_id=-1
),导致每次请求都查数据库。 -
解决方案:
-
缓存空值:查询结果为空时,缓存
null
(设置短过期时间,如 5min)。 -
布隆过滤器(Bloom Filter):预先存储所有存在的
user_id
,查询前判断是否存在(示例 Redisson 布隆过滤器):RBloomFilter<Long> bloomFilter = redisson.getBloomFilter("user_bloom_filter"); bloomFilter.tryInit(1000000L, 0.01); // 预计插入 100 万条,误判率 1% for (Long userId : allUserIds) {bloomFilter.add(userId); } if (!bloomFilter.contains(userId)) {return null; // 不存在,直接返回 }
-
-
-
缓存击穿:
-
问题:热点 key(如
product:123
)过期时,大量请求同时查数据库。 -
解决方案:
-
永不过期:设置逻辑过期时间(如记录
expire_time
字段,查询时判断是否过期)。 -
互斥锁(Mutex):使用 Redis
SETNX
锁,仅允许一个线程查数据库(示例):String lockKey = "lock:user:123"; if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS)) {try {// 查数据库并更新缓存} finally {redisTemplate.delete(lockKey);} } else {// 重试或返回旧值 }
-
-
-
缓存雪崩:
- 问题:大量 key 同时过期,导致数据库压力骤增。
- 解决方案:为不同 key 设置随机过期时间(如 30min~1h)。
3. 多级缓存架构
-
流程设计:
- 用户请求 → 本地缓存(Caffeine):命中则返回。
- 未命中 → 分布式缓存(Redis):命中则返回,并回种本地缓存。
- 未命中 → 数据库:查询后回种 Redis 和本地缓存。
-
代码示例:
public User getUser(Long userId) {// 1. 查本地缓存User user = localCache.getIfPresent(userId);if (user != null) {return user;}// 2. 查分布式缓存user = redisTemplate.opsForValue().get("user:" + userId);if (user != null) {localCache.put(userId, user); // 回种本地缓存return user;}// 3. 查数据库user = userDao.getUserById(userId);if (user != null) {redisTemplate.opsForValue().set("user:" + userId, user, 30, TimeUnit.MINUTES); // 回种 RedislocalCache.put(userId, user); // 回种本地缓存}return user; }
五、异步与消息队列:解耦耗时操作
通过消息队列将非实时操作(如日志、通知)异步处理,释放主线程处理新请求。
1. 消息队列选型对比
特性 | Kafka | RocketMQ | RabbitMQ |
---|---|---|---|
吞吐量 | 百万级 TPS(顺序写磁盘) | 十万级 TPS(支持事务) | 万级 TPS(基于内存) |
消息顺序 | 分区内有序 | 全局有序(单分区) | 不保证全局有序 |
消息可靠性 | 至少一次(需消费者确认) | 至少一次(支持事务回滚) | 至少一次(支持死信队列) |
适用场景 | 日志收集、大数据流处理 | 电商交易、金融支付 | 小规模通知、任务调度 |
2. Kafka 高级实践
-
分区与副本:
- 分区数:根据消费者数量设置(如 3 个消费者设 3 个分区,提高并行度)。
- 副本数:设置为 3(主副本 + 2 个从副本),防止单节点故障。
-
消费者组:
- 广播消费:每个消费者接收全量消息(
enable.auto.commit=false
,手动提交偏移量)。 - 集群消费:消息被组内一个消费者消费(默认模式)。
- 广播消费:每个消费者接收全量消息(
-
示例生产者:
Properties props = new Properties(); props.put("bootstrap.servers", "kafka1.example.com:9092,kafka2.example.com:9092"); props.put("acks", "all"); // 所有副本确认 props.put("retries", 3); // 重试次数 props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");KafkaProducer<String, String> producer = new KafkaProducer<>(props); ProducerRecord<String, String> record = new ProducerRecord<>("order_topic", "key", JSON.toJSONString(order)); producer.send(record, (metadata, exception) -> {if (exception != null) {// 处理发送失败(如记录日志、重试)} }); producer.close();
3. RocketMQ 事务消息
-
原理:通过两阶段提交(2PC)保证消息与数据库操作的原子性。
-
流程:
- 发送半消息(
PREPARED
状态,消费者不可见)。 - 执行本地事务(如扣减库存)。
- 根据事务结果提交(
COMMIT
)或回滚(ROLLBACK
)消息。
- 发送半消息(
-
示例代码:
TransactionMQProducer producer = new TransactionMQProducer("order_producer_group"); producer.setNamesrvAddr("rocketmq-namesrv:9876"); producer.start();TransactionSendResult result = producer.sendMessageInTransaction(new Message("order_topic", "TAG_A", JSON.toJSONString(order).getBytes()),new LocalTransactionExecutor() {@Overridepublic LocalTransactionState executeLocalTransactionBranch(Message msg, Object arg) {// 执行本地事务(扣减库存)boolean success = inventoryService.deductStock(msg.getProperty("productId"));return success ? LocalTransactionState.COMMIT_MESSAGE : LocalTransactionState.ROLLBACK_MESSAGE;}},null );
六、限流降级与容灾:保护系统稳定性
高并发下需防止系统过载,通过限流、降级、熔断等机制保障核心业务可用。
1. 限流(Rate Limiting)
-
Sentinel 滑动窗口算法:
-
原理:将时间窗口划分为多个小窗口(如 1 秒分为 10 个 100ms 窗口),统计每个小窗口的请求数,滑动窗口平滑流量。
-
示例配置(控制台定义规则):
{"resource": "order_api","limitApp": "default","grade": 1, // 1=QPS 限流,0=线程数限流"count": 1000, // 每秒最多 1000 次请求"timeWindow": 1, // 时间窗口 1 秒"strategy": 0, // 0=直接拒绝,1=Warm Up,2=排队等待"controlBehavior": 0 }
-
集成 Spring Boot:
@RestController public class OrderController {@GetMapping("/order")@SentinelResource(value = "order_api", blockHandler = "handleBlock")public String createOrder() {return "下单成功";}public String handleBlock(BlockException ex) {return "当前流量过大,请稍后再试";} }
-
2. 降级(Degradation)
-
Sentinel 阈值降级:
-
原理:监控服务的错误率(如 > 50%)或平均响应时间(如 > 3s),触发降级(返回默认值或空)。
-
示例配置(控制台定义规则):
{"resource": "payment_service","grade": 0, // 0=错误率降级,1=平均响应时间降级"count": 50, // 错误率阈值 50%"timeWindow": 10, // 统计时间窗口 10 秒"minRequestAmount": 5 // 最小请求数(避免偶发波动) }
-
-
Hystrix 熔断降级(已停更,仅作参考):
@HystrixCommand(fallbackMethod = "fallbackPay",commandProperties = {@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), // 10 秒内至少 10 次请求@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"), // 错误率 > 50% 触发熔断@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000") // 熔断 5 秒后尝试恢复} ) public String pay(Order order) {return paymentService.pay(order); // 调用支付服务 }public String fallbackPay(Order order) {return "支付服务繁忙,请稍后再试"; }
3. 熔断(Circuit Breaker)
-
Resilience4J 熔断(Sentinel 的替代方案):
-
核心状态:CLOSED(正常)、OPEN(熔断)、HALF_OPEN(半开,尝试恢复)。
-
示例代码:
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("paymentService"); CheckedFunction0<String> decoratedSupplier = CircuitBreaker.decorateCheckedSupplier(circuitBreaker, () -> paymentService.pay(order)); Try<String> result = Try.of(decoratedSupplier).recover(ex -> "支付失败:" + ex.getMessage());
-
七、云原生与弹性伸缩:应对流量波动
云原生技术通过容器化、自动化扩缩容,灵活应对流量高峰与低谷。
1. 容器化(Docker/K8s)
-
Docker 镜像构建:
-
多阶段构建:减小镜像体积(示例Dockerfile):
# 构建阶段 FROM maven:3.8.6 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests# 运行阶段 FROM openjdk:17-jdk-slim WORKDIR /app COPY --from=builder /app/target/app.jar . EXPOSE 8080 CMD ["java", "-jar", "app.jar"]
-
-
Kubernetes 弹性伸缩:
-
HPA(Horizontal Pod Autoscaler):基于 CPU/内存或自定义指标(如 QPS)自动扩缩 Pod 数量(示例):
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata:name: app-hpa spec:scaleTargetRef:apiVersion: apps/v1kind: Deploymentname: app-deploymentminReplicas: 2maxReplicas: 10metrics:- type: Resourceresource:name: cputarget:type: UtilizationaverageUtilization: 70 # CPU 使用率超 70% 时扩容
-
2. Serverless(无服务器)
-
AWS Lambda:
-
适用场景:突发流量(如秒杀活动)、定时任务(如每日数据统计)。
-
示例代码(Python):
import jsondef lambda_handler(event, context):# 处理秒杀请求item_id = event['queryStringParameters']['itemId']stock = get_stock_from_dynamodb(item_id) # 调用 DynamoDBif stock > 0:deduct_stock(item_id) # 扣减库存return {'statusCode': 200,'body': json.dumps('秒杀成功')}else:return {'statusCode': 400,'body': json.dumps('库存不足')}
-
-
阿里云函数计算(FC):
- 优势:与阿里云其他服务(OSS、RDS)深度集成,支持事件触发(如 OSS 文件上传触发函数)。
总结:高并发系统的组合策略
高并发系统需根据业务场景 多维度优化,以下是常见场景的最佳实践:
场景 | 核心方案 |
---|---|
静态内容为主(新闻网站) | HTML 静态化 + CDN 加速 + 图片服务器分离 + 浏览器缓存 |
动态交互为主(电商) | 负载均衡(Nginx/LVS) + 数据库分库分表 + 分布式缓存(Redis) + 异步消息队列(Kafka) |
突发流量(秒杀) | 限流降级(Sentinel) + 弹性伸缩(K8s/Serverless) + 本地缓存(Caffeine) + 消息队列削峰 |
高一致性要求(金融) | 分布式事务(Seata) + 缓存一致性(双写+失效) + 数据库主从复制 + 读写分离 |
关键原则:
- 优先通过缓存、异步、静态化减少动态请求;
- 数据库是瓶颈,需尽早分库分表;
- 监控(Prometheus + Grafana)和压测(JMeter)是优化的前提,需持续观察系统瓶颈。
🔴🟠🟡🟢🔵🟣🔴🔴🟠🟡🟢🔵🟣🔴🔴🟠🟡🟢🔵🟣🔴🔴🟠🟡🟢🔵🟣🔴