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)是并发性能问题中最常见的根因。衡量锁竞争严重程度的指标:

  • synchronized Monitor 竞争率:通过 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 到乐观读 中介绍的 ReentrantReadWriteLockStampedLock 替代互斥锁:

// 读多写少的配置缓存:用 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 生成):AtomicLongLongAdder.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;

LongAdderCell 类就是用 @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 条件):

  1. 互斥(Mutual Exclusion):资源同一时间只能被一个线程持有
  2. 持有并等待(Hold and Wait):线程持有资源,同时等待其他资源
  3. 不可抢占(No Preemption):资源不能被强制夺走,只能由持有者主动释放
  4. 循环等待(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 万并发请求同时扣减库存。要求:

  1. 正确性:库存不能超卖(最终库存 ≥ 0)
  2. 性能:P99 响应时间 < 100ms
  3. 可用性:不能因锁竞争导致服务宕机

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 密集型,旧版本 JDKThreadPoolExecutor + 合理配置
异步流水线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,而是在每个架构决策点上,清楚地知道为什么这样选择,代价是什么,边界在哪里


参考文献

  1. Goetz et al., “Java Concurrency in Practice”(全书,强烈推荐反复阅读)
  2. Herlihy & Shavit, “The Art of Multiprocessor Programming”
  3. 陈皓, “高并发的哲学原理”, coolshell.cn
  4. 美团技术博客, “不可不说的Java锁事”, 2018
  5. 美团技术博客, “Java线程池实现原理及其在美团业务中的实践”, 2020
  6. Martin Thompson, “LMAX Disruptor: High Performance Inter-Thread Messaging Library”
  7. Brendan Gregg, “Systems Performance: Enterprise and the Cloud”, 2nd Ed.

思考题

  1. 在高并发计数器场景中,AtomicLongLongAdder → 线程本地计数 + 汇总,三种方案的吞吐量依次提升但精确性依次降低。在一个’每秒需要统计千万次请求的 QPS 指标’场景中,你选择哪种方案?如果需要同时支持’实时查看 QPS’和’精确统计总请求数’,你如何设计?
  2. 无锁编程(Lock-Free)通常使用 CAS 循环实现。CAS 循环在高竞争下会’空转’(spin),消耗 CPU。在什么竞争程度下,CAS 循环的 CPU 开销会超过使用锁的线程切换开销?有没有一种’自适应’的策略——低竞争时 CAS,高竞争时降级为锁?
  3. 减小锁粒度是提升并发性能的常见手段——从一把全局锁到分段锁,再到每个元素一把锁。但锁粒度越细,锁的数量越多,内存开销也越大。在一个有 1000 万个对象需要并发访问的场景中,你如何在锁粒度和内存开销之间找到平衡点?有没有不依赖锁的替代方案(如 Actor 模型)?