17 实战——高并发场景下的锁优化与无锁编程
摘要:
前 16 篇系统地剖析了 Java 并发编程的每一个核心组件,从 CPU 缓存到 JMM、从 volatile 到 AQS、从线程池到虚拟线程。本篇是全专栏的压轴实战,将这些知识点融合为一套系统性的并发性能分析与优化方法论。高并发问题的本质是竞争——对 CPU、内存、锁、IO 的竞争。本文通过五个典型的生产性能问题场景,逐一分析竞争的根因,并给出对应的优化策略:从锁粒度优化(分段锁、锁分离)到无锁方案(CAS、无锁数据结构),从热点数据消除(分散写、合并读)到内存布局优化(伪共享、对象池)。同时深入讨论死锁、活锁、饥饿三类并发活性问题的检测与处理,以及如何建立一套完整的并发系统监控体系。
第 1 章 并发性能问题的分类与分析方法
1.1 并发问题的两个维度
并发问题可以从两个维度分类:
安全性(Safety):程序的执行结果是否正确?数据是否会错乱?这包括竞态条件(Race Condition)、数据不一致(Data Inconsistency)、可见性问题(Visibility Bug)等。安全性问题的后果是”程序给出了错误的结果”。
活性(Liveness):程序是否能够持续向前推进?这包括死锁(Deadlock,所有线程永远阻塞)、活锁(Livelock,线程持续活动但没有进展)、饥饿(Starvation,某线程长期无法获得资源)等。活性问题的后果是”程序停止推进”或”部分任务永远得不到执行”。
性能(Performance):程序是否能够在可接受的时间内完成任务?这包括锁竞争(Lock Contention)、上下文切换开销、伪共享(False Sharing)、热点数据竞争等。性能问题的后果是”程序虽然正确,但太慢”。
本文主要关注后两类,尤其是性能类问题——这是生产环境中更常见、更难排查的类型。
1.2 并发性能分析的工具链
在深入优化之前,必须先准确定位问题。以下是常用的分析工具:
jstack + 线程 dump 分析:
# 每隔 3 秒抓一次线程 dump,连续 3 次,用于分析哪些线程长期阻塞
for i in 1 2 3; do
jstack <pid> > thread_dump_$i.txt
sleep 3
done
# 分析:如果同一线程在 3 次 dump 中都处于 BLOCKED 或 WAITING 状态,说明存在竞争或等待问题JFR(Java Flight Recorder):
# 录制 60 秒的 JFR 事件(JDK 11+ 免费可用)
jcmd <pid> JFR.start name=perf settings=profile duration=60s filename=perf.jfr
# 用 JDK Mission Control 分析 perf.jfr
# 重点关注:
# - Lock Instances(锁竞争最激烈的对象)
# - Monitor Inflation(synchronized 重量化事件)
# - Thread Sleep / Park(线程等待时间分布)Async-Profiler(采样型 profiler):
# 生成火焰图,直观看到 CPU 时间和线程阻塞分布在哪
./profiler.sh -e cpu -d 30 -f cpu_flame.html <pid>
./profiler.sh -e lock -d 30 -f lock_flame.html <pid>Arthas(阿里巴巴开源的 Java 诊断工具):
# 找出热点方法
trace com.example.OrderService processOrder
# 查看某个类的方法执行耗时分布
watch com.example.UserService findById '{params, returnObj, throwExp}' -x 2原则:先 profiling,再优化。没有数据支撑的优化是猜测,可能优化了不是瓶颈的地方,浪费时间甚至引入新问题。
第 2 章 锁竞争优化:减少、缩短、分散
2.1 锁竞争的量化指标
锁竞争(Lock Contention)是并发性能问题中最常见的根因。衡量锁竞争严重程度的指标:
synchronizedMonitor 竞争率:通过 JFR 的jdk.JavaMonitorEnter事件,查看duration > 0(需要等待)的占比- 线程 dump 中 BLOCKED 状态线程数:比例越高,竞争越激烈
- 平均锁等待时间:Async-Profiler 的
-e lock模式可以直接测量
2.2 策略 1:减少锁的持有时间
锁持有时间越长,其他线程等待的时间越长,竞争越激烈。优化的第一步是缩短临界区:
// 反面教材:临界区包含了不必要的操作
public synchronized void processOrder(Order order) {
validateOrder(order); // 纯计算,不需要在锁内
String enriched = enrichData(order); // 调用外部服务,IO!不应在锁内
orders.add(enriched); // 只有这行需要锁保护
sendNotification(order); // IO 操作,不需要在锁内
logAudit(order); // 日志 IO,不需要在锁内
}
// 优化:只保护真正需要保护的操作
public void processOrder(Order order) {
validateOrder(order); // 锁外:纯计算
String enriched = enrichData(order); // 锁外:IO 操作
synchronized (this) {
orders.add(enriched); // 锁内:最小临界区
}
sendNotification(order); // 锁外:IO
logAudit(order); // 锁外:日志
}2.3 策略 2:锁分段(Lock Striping)
ConcurrentHashMap 的 JDK 8 实现是锁分段的最佳范例——不是锁整个数据结构,而是只锁被操作的那个桶(bucket)。
自定义锁分段的实现思路:
// 需要保护一个大数组,不同下标的操作互不干扰
public class StripedLockArray<T> {
private final int stripes;
private final ReentrantLock[] locks;
private final T[] data;
@SuppressWarnings("unchecked")
public StripedLockArray(int size, int stripes) {
this.stripes = stripes;
this.locks = new ReentrantLock[stripes];
this.data = (T[]) new Object[size];
for (int i = 0; i < stripes; i++) {
locks[i] = new ReentrantLock();
}
}
// 根据下标计算使用哪把锁
private ReentrantLock lockFor(int index) {
return locks[index % stripes]; // 将 index 散列到 stripes 个锁上
}
public void set(int index, T value) {
ReentrantLock lock = lockFor(index);
lock.lock();
try {
data[index] = value;
} finally {
lock.unlock();
}
}
public T get(int index) {
ReentrantLock lock = lockFor(index);
lock.lock();
try {
return data[index];
} finally {
lock.unlock();
}
}
}分段数的选择:通常设为 CPU 核心数的 2~4 倍(确保在高并发下,不同线程大概率使用不同的段锁,减少竞争)。
2.4 策略 3:读写分离
如果数据是读多写少,用 08 读写锁与 StampedLock——从 ReentrantReadWriteLock 到乐观读 中介绍的 ReentrantReadWriteLock 或 StampedLock 替代互斥锁:
// 读多写少的配置缓存:用 StampedLock 乐观读最大化读吞吐量
public class ConfigStore {
private final StampedLock sl = new StampedLock();
private Map<String, String> config = new HashMap<>();
// 读:乐观读(无锁,几乎零代价)
public String get(String key) {
long stamp = sl.tryOptimisticRead();
String value = config.get(key);
if (!sl.validate(stamp)) {
// 有写操作发生,升级为悲观读
stamp = sl.readLock();
try {
value = config.get(key);
} finally {
sl.unlockRead(stamp);
}
}
return value;
}
// 写:独占写锁
public void update(Map<String, String> newConfig) {
long stamp = sl.writeLock();
try {
this.config = new HashMap<>(newConfig);
} finally {
sl.unlockWrite(stamp);
}
}
}第 3 章 热点数据的无锁优化
3.1 热点数据问题:全局计数器的困境
最典型的热点数据问题是全局计数器——多个线程同时更新同一个计数值:
// 方案 1:synchronized 全局锁(最差)
private long count = 0;
public synchronized void increment() { count++; }
// 方案 2:AtomicLong CAS(好,但高并发下仍有竞争)
private AtomicLong count = new AtomicLong(0);
public void increment() { count.incrementAndGet(); }
// 方案 3:LongAdder 分段(最佳,低竞争+高吞吐)
private LongAdder count = new LongAdder();
public void increment() { count.increment(); }
public long getCount() { return count.sum(); }如 05 CAS 与原子类——Unsafe、AtomicInteger 到 LongAdder 的演进 所述,LongAdder 通过 Cell[] 分散写热点,在高并发写场景下吞吐量是 AtomicLong 的 5~10 倍。
LongAdder vs AtomicLong 的选型:
- 需要精确的原子读写(如实现唯一 ID 生成):
AtomicLong(LongAdder.sum()不是精确快照) - 只需要统计计数(监控指标、性能统计、请求计数):
LongAdder
3.2 无锁链表与无锁队列
ConcurrentLinkedQueue 是 JDK 中无锁(lock-free)队列的代表实现,基于 Michael & Scott 无锁队列算法。其核心是用 CAS 操作替代锁:
// ConcurrentLinkedQueue.offer() 的简化逻辑
public boolean offer(E e) {
final Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) {
// p 是真正的尾节点,CAS 将新节点接在 p 后面
if (NEXT.compareAndSet(p, null, newNode)) {
// 成功:更新 tail(允许 tail 落后真正的尾节点最多 1 步)
if (p != t)
TAIL.weakCompareAndSet(this, t, newNode);
return true;
}
// CAS 失败:其他线程已经插入了节点,重试
} else if (p == q) {
// 队列处于特殊状态(并发 poll 时),重新从 head/tail 定位
p = (t != (t = tail)) ? t : head;
} else {
// 向前追赶真正的尾节点
p = (p != t && t != (t = tail)) ? t : q;
}
}
}无锁算法的特点:
- 无锁(lock-free):系统整体总是向前推进(即使某个线程暂停,其他线程仍能继续)
- 高吞吐:没有锁竞争和上下文切换
- 代价:ABA 问题(需要版本号或
AtomicStampedReference解决);实现复杂,难以验证正确性;在低竞争场景下可能不如简单锁快(CAS 重试有开销)
3.3 不可变对象与写时复制
另一类消除锁的思路是不可变性:如果数据一旦创建就不改变,多线程并发读不需要任何同步。
// 场景:频繁读、偶尔整体替换的配置数据
public class AppConfig {
// volatile 保证新 Config 对象对所有线程可见
private volatile Config config = loadConfig();
// 读:完全无锁(volatile 读 + 不可变 Config 对象)
public String getValue(String key) {
return config.get(key); // Config 是不可变的,直接读取
}
// 写:创建新的 Config 对象,原子地替换引用
public void reload() {
Config newConfig = loadConfig(); // 加载新配置(IO,在锁外)
this.config = newConfig; // 原子赋值(volatile 写)
// 旧的 Config 对象:如果没有其他引用,GC 会回收
}
}
// Config 类:不可变(所有字段 final)
public final class Config {
private final Map<String, String> data;
Config(Map<String, String> data) {
this.data = Collections.unmodifiableMap(new HashMap<>(data)); // 防御性复制
}
public String get(String key) { return data.get(key); }
}这与 10 并发容器(下)——CopyOnWriteArrayList、BlockingQueue 家族 中 CopyOnWriteArrayList 的思想完全一致:用”版本快照”替代”锁保护的可变状态”,让读操作彻底无锁。
第 4 章 伪共享(False Sharing)——隐形的缓存行杀手
4.1 伪共享的根因
如 01 并发编程的硬件基础——CPU 缓存、MESI 与内存屏障 所述,CPU 缓存以缓存行(Cache Line,通常 64 字节) 为单位操作数据。伪共享是指:两个不相关的变量恰好位于同一个缓存行中,导致对其中一个的写操作引发另一个的缓存失效,即使这两个变量没有逻辑关系。
典型场景:多线程并发修改一个数组的不同元素:
// 每个线程负责更新 counts[i](各自独立)
long[] counts = new long[NUM_THREADS];
// 假设 NUM_THREADS = 8,每个 long 8 字节
// counts[0] 和 counts[1] 共享同一个 64 字节缓存行(8 个 long = 64 字节)
// 线程 0 更新 counts[0],触发缓存行失效 → 线程 1 的 counts[1] 也失效!
// 即使 counts[0] 和 counts[1] 逻辑上完全独立,它们仍然相互干扰4.2 解决伪共享:缓存行填充
方法 1:@Contended 注解(JDK 8+)
// @jdk.internal.vm.annotation.Contended 告诉 JVM 在此字段前后填充缓存行
// 需要 JVM 参数 -XX:-RestrictContended 开放使用(JDK 内部默认可用)
@jdk.internal.vm.annotation.Contended
private volatile long value;LongAdder 的 Cell 类就是用 @Contended 避免不同 Cell 之间的伪共享(见 05 CAS 与原子类——Unsafe、AtomicInteger 到 LongAdder 的演进)。
方法 2:手动填充(老版本兼容)
// 手动填充:在字段前后加 7 个 long(7 × 8 = 56 字节),确保独占一个缓存行
abstract class PaddedLong {
long p1, p2, p3, p4, p5, p6, p7; // 前置填充(56 字节)
volatile long value; // 有效字段(8 字节)
long q1, q2, q3, q4, q5, q6, q7; // 后置填充(56 字节)
// 总共 64 × 2 = 128 字节,value 独占中间 64 字节的缓存行
}Disruptor 框架的极致优化:LMAX Disruptor 是基于环形缓冲区的高性能无锁队列,对缓存行填充有大量应用,实测在高并发下比 ArrayBlockingQueue 快 5~10 倍。它的 Sequence 类就是经典的手动填充实现。
第 5 章 死锁、活锁与饥饿的检测和处理
5.1 死锁:四个必要条件
死锁发生需要同时满足四个条件(Coffman 条件):
- 互斥(Mutual Exclusion):资源同一时间只能被一个线程持有
- 持有并等待(Hold and Wait):线程持有资源,同时等待其他资源
- 不可抢占(No Preemption):资源不能被强制夺走,只能由持有者主动释放
- 循环等待(Circular Wait):存在线程等待链形成的环
打破任意一个条件即可预防死锁:
打破”持有并等待”:一次性获取所有资源(但可能导致利用率低)
打破”循环等待”:对资源排序,所有线程按相同顺序申请资源(最常用的实践方案):
// 银行转账:按账户 ID 排序获取锁,避免死锁
void transfer(Account from, Account to, int amount) {
// 按 ID 从小到大获取锁,保证所有线程的加锁顺序一致
Account first = from.getId() < to.getId() ? from : to;
Account second = from.getId() < to.getId() ? to : from;
synchronized (first) {
synchronized (second) {
from.balance -= amount;
to.balance += amount;
}
}
}打破”不可抢占”:使用可超时的 tryLock:
// 使用 tryLock 超时,避免死锁
public boolean transfer(Account from, Account to, int amount) {
while (true) {
if (from.lock.tryLock(50, TimeUnit.MILLISECONDS)) {
try {
if (to.lock.tryLock(50, TimeUnit.MILLISECONDS)) {
try {
from.balance -= amount;
to.balance += amount;
return true;
} finally {
to.lock.unlock();
}
}
} finally {
from.lock.unlock();
}
}
// 两个锁都获取失败,等待随机时间后重试(指数退避)
Thread.sleep(ThreadLocalRandom.current().nextInt(10));
}
}5.2 死锁检测
线程 dump 分析:
jstack <pid> | grep -A 20 "deadlock"
# 或者 jstack 自带死锁检测:
# "Found one Java-level deadlock:" 会直接标出死锁线程和等待的锁MXBean 编程式检测:
// 编程式检测死锁(可用于自动化监控)
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] deadlockedIds = bean.findDeadlockedThreads();
if (deadlockedIds != null) {
ThreadInfo[] infos = bean.getThreadInfo(deadlockedIds, true, true);
for (ThreadInfo info : infos) {
log.error("死锁线程: {} 等待锁: {}", info.getThreadName(), info.getLockName());
}
// 触发告警
alertSystem.deadlockDetected(infos);
}5.3 活锁:持续活动却没有进展
活锁(Livelock)比死锁更难发现:线程没有阻塞,一直在执行,但永远无法完成任务。典型例子是两个线程互相让步:
线程 A 尝试获取锁 1,发现锁 2 被 B 持有,放弃,等待
线程 B 尝试获取锁 2,发现锁 1 被 A 持有,放弃,等待
线程 A 重试,发现锁 2 仍被 B 持有,再次放弃...
(无限循环,双方都"礼让")
解决活锁:引入随机退避(Random Backoff)——每次重试前等待随机时间,打破对称性:
int retries = 0;
while (!tryAcquireBothLocks()) {
// 指数退避 + 随机抖动,避免两个线程同步重试
long backoff = (long)(Math.pow(2, retries) * 10) + ThreadLocalRandom.current().nextInt(10);
Thread.sleep(Math.min(backoff, 1000)); // 最大退避 1 秒
retries++;
}以太网的 CSMA/CD 协议正是用这个思路解决碰撞问题(Binary Exponential Backoff)。
5.4 饥饿:某线程长期无法获得资源
成因:
- 不公平锁(
synchronized或非公平ReentrantLock)在高竞争下,某些线程可能长期被”插队” - 优先级设置不当(低优先级线程在高优先级线程持续存在时几乎得不到 CPU)
- 无界队列 + 高提交速率(新任务不断加入,老任务永远排不到)
解决方案:
- 对饥饿敏感的场景使用公平锁(
new ReentrantLock(true)),牺牲少量吞吐量换取公平性 - 有界队列 + 背压(限制提交速率,防止任务无限积压)
- 监控等待时间 SLA:设置任务等待超时,超时任务直接失败或升级处理
第 6 章 综合实战案例:电商高峰期秒杀库存扣减
6.1 问题描述
场景:电商秒杀活动,1000 件商品,10 万并发请求同时扣减库存。要求:
- 正确性:库存不能超卖(最终库存 ≥ 0)
- 性能:P99 响应时间 < 100ms
- 可用性:不能因锁竞争导致服务宕机
6.2 方案演进
方案 0:直接 synchronized(基准)
public synchronized boolean deductStock(String productId, int quantity) {
int stock = stockMap.get(productId);
if (stock < quantity) return false;
stockMap.put(productId, stock - quantity);
return true;
}
// 10 万 QPS,synchronized 变成单点瓶颈,P99 >> 1 秒,不可用方案 1:数据库乐观锁(常见方案)
-- 用版本号实现乐观锁
UPDATE stock SET quantity = quantity - ?, version = version + 1
WHERE product_id = ? AND version = ? AND quantity >= ?// 重试逻辑
public boolean deductStock(String productId, int quantity) {
for (int retry = 0; retry < 3; retry++) {
StockRecord record = stockDao.findByProductId(productId);
if (record.getQuantity() < quantity) return false;
int affected = stockDao.updateWithVersion(productId, quantity, record.getVersion());
if (affected == 1) return true; // 成功
// affected == 0:版本冲突,重试
}
return false; // 重试失败
}
// 问题:10 万 QPS 打到数据库,数据库连接池成为瓶颈,且大量重试放大数据库压力方案 2:Redis + Lua 原子脚本(生产常用方案)
-- Lua 脚本在 Redis 中原子执行(Redis 单线程,Lua 原子)
local current = tonumber(redis.call('GET', KEYS[1]))
if current == nil then return -1 end -- 商品不存在
if current < tonumber(ARGV[1]) then return 0 end -- 库存不足
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1 -- 成功// 使用 Redis Lua 脚本实现原子扣减
public boolean deductStock(String productId, int quantity) {
Long result = jedis.eval(DEDUCT_SCRIPT,
Collections.singletonList("stock:" + productId),
Collections.singletonList(String.valueOf(quantity)));
return result != null && result == 1;
}
// Redis 单节点 QPS > 10 万,Lua 原子,不超卖,延迟 < 1ms
// 问题:Redis 宕机时库存数据丢失,需要定期同步到数据库方案 3:分段库存(最高性能,复杂度最高)
将 1000 件库存分散到 N 个”库存桶”(如 10 个桶,每桶 100 件)。请求随机选择一个桶扣减,桶满则尝试其他桶:
class SegmentedStock {
private static final int SEGMENTS = 16;
private final AtomicInteger[] segments = new AtomicInteger[SEGMENTS];
SegmentedStock(int total) {
int perSegment = total / SEGMENTS;
for (int i = 0; i < SEGMENTS; i++) {
segments[i] = new AtomicInteger(perSegment);
}
}
public boolean deduct(int quantity) {
// 随机选择起始桶,顺序尝试(减少竞争,避免热点)
int start = ThreadLocalRandom.current().nextInt(SEGMENTS);
for (int i = 0; i < SEGMENTS; i++) {
int idx = (start + i) % SEGMENTS;
int current;
while ((current = segments[idx].get()) >= quantity) {
if (segments[idx].compareAndSet(current, current - quantity)) {
return true; // 扣减成功
}
}
// 此桶不够,试下一个
}
return false; // 所有桶都不够,库存不足
}
}
// 16 路并发,CAS 无锁,理论吞吐量是方案 0 的 16 倍
// 问题:总库存计算需要汇总所有桶;可能出现"某桶为 0 但其他桶有余量"的不均衡6.3 方案选型建议
| 场景 | 推荐方案 |
|---|---|
| 低并发(< 1000 QPS) | 数据库乐观锁(简单可靠) |
| 中等并发(1000~5 万 QPS) | Redis 原子脚本(低延迟,一致性好) |
| 超高并发(> 5 万 QPS) | Redis + 分段库存(最高性能) |
| 对一致性要求极高(金融级) | 数据库串行化隔离 + 分库分表 |
第 7 章 建立并发系统的监控体系
7.1 关键监控指标
一个完整的并发系统监控体系应该包含:
线程池监控:
// 注册线程池指标到 Prometheus/Micrometer
MeterRegistry registry = /* ... */;
Gauge.builder("threadpool.active", executor, ThreadPoolExecutor::getActiveCount)
.tag("pool", "biz-handler").register(registry);
Gauge.builder("threadpool.queue.size", executor, e -> e.getQueue().size())
.tag("pool", "biz-handler").register(registry);
Counter.builder("threadpool.rejected")
.tag("pool", "biz-handler").register(registry);报警规则:
- 队列使用率 > 80%:预警,处理速率落后
- 活跃线程数 / 最大线程数 > 0.9:濒临饱和
- 拒绝数 > 0:已经开始丢弃任务,立即告警
锁竞争监控:
通过 JFR 持续录制,将 jdk.JavaMonitorEnter 事件的平均等待时间作为指标:超过 1ms 的等待说明存在明显竞争。
GC 压力监控:
高并发下频繁创建对象(如 CopyOnWriteArrayList 的写操作,每次 LinkedBlockingQueue.put 创建 Node)会引发频繁 GC,间接导致 Stop-The-World 停顿影响响应时间。关注 YGC 频率和时间。
第 8 章 总结:高并发优化方法论
经过 17 篇的系统学习,这里总结一套可操作的并发性能优化方法论:
第一步:量化问题
不要假设,先度量。用 JFR、Async-Profiler、Arthas 找到真正的瓶颈——是锁竞争、CPU 热点、IO 等待还是 GC?
第二步:选择正确的并发模型
| 任务类型 | 推荐模型 |
|---|---|
| IO 密集型,高并发连接 | 虚拟线程(JDK 21+) |
| CPU 密集型,可分治 | ForkJoinPool |
| IO 密集型,旧版本 JDK | ThreadPoolExecutor + 合理配置 |
| 异步流水线 | CompletableFuture + 专用线程池 |
第三步:优化锁策略
锁开销大?
→ 缩短临界区(移除锁内的 IO 操作)
→ 锁分段(数据水平分割,每段独立加锁)
→ 读写分离(`ReentrantReadWriteLock` 或 `StampedLock`)
→ 无锁算法(`AtomicXxx`、`LongAdder`、`CAS`)
→ 不可变 + 写时复制(消灭可变共享状态)
第四步:消除热点
热点数据?
→ 分散写:LongAdder、分段库存
→ 合并读:批量查询,缓存
→ 版本快照:volatile 引用 + 不可变对象
第五步:优化内存布局
CPU 性能不如预期?
→ 检查伪共享:@Contended 或手动填充
→ 对象池化:减少 GC 压力(连接池、线程池、对象池)
→ 减少装箱拆箱:用 int/long 替代 Integer/Long(尤其在高频路径)
后记
至此,Java 并发编程专栏的 17 篇文章全部完成。
这个专栏的写作逻辑遵循了一条清晰的主线:从硬件基础(CPU 缓存、MESI)到语言规范(JMM、happens-before),从同步原语(volatile、synchronized、CAS)到框架工具(AQS、ReentrantLock、读写锁),从数据结构(ConcurrentHashMap、BlockingQueue)到线程管理(ThreadPoolExecutor、ForkJoinPool),从异步编程(CompletableFuture)到未来技术(虚拟线程)。
每一层都建立在上一层的基础上,最终在本篇实战中汇聚为可操作的工程方法论。
理解这条主线,是从”会用并发 API”进阶到”懂并发设计”的关键。真正的高并发系统设计,不是堆叠 API,而是在每个架构决策点上,清楚地知道为什么这样选择,代价是什么,边界在哪里。
参考文献
- Goetz et al., “Java Concurrency in Practice”(全书,强烈推荐反复阅读)
- Herlihy & Shavit, “The Art of Multiprocessor Programming”
- 陈皓, “高并发的哲学原理”, coolshell.cn
- 美团技术博客, “不可不说的Java锁事”, 2018
- 美团技术博客, “Java线程池实现原理及其在美团业务中的实践”, 2020
- Martin Thompson, “LMAX Disruptor: High Performance Inter-Thread Messaging Library”
- Brendan Gregg, “Systems Performance: Enterprise and the Cloud”, 2nd Ed.
思考题
- 在高并发计数器场景中,
AtomicLong→LongAdder→ 线程本地计数 + 汇总,三种方案的吞吐量依次提升但精确性依次降低。在一个’每秒需要统计千万次请求的 QPS 指标’场景中,你选择哪种方案?如果需要同时支持’实时查看 QPS’和’精确统计总请求数’,你如何设计?- 无锁编程(Lock-Free)通常使用 CAS 循环实现。CAS 循环在高竞争下会’空转’(spin),消耗 CPU。在什么竞争程度下,CAS 循环的 CPU 开销会超过使用锁的线程切换开销?有没有一种’自适应’的策略——低竞争时 CAS,高竞争时降级为锁?
- 减小锁粒度是提升并发性能的常见手段——从一把全局锁到分段锁,再到每个元素一把锁。但锁粒度越细,锁的数量越多,内存开销也越大。在一个有 1000 万个对象需要并发访问的场景中,你如何在锁粒度和内存开销之间找到平衡点?有没有不依赖锁的替代方案(如 Actor 模型)?