跳转到内容
123xiao | 无名键客

《Java开发踩坑实录:8个高频并发问题的排查思路与修复方案》

字数: 0 阅读时长: 1 分钟

背景与问题

并发问题最烦人的地方,不是它“难”,而是它偶发、隐蔽、难复现。你本地跑 100 次没事,一上线在流量高峰就炸;日志里看起来都正常,但结果就是少数据、重复扣款、线程池打满、接口超时。

我自己做 Java 服务时,最怕看到这几类告警:

  • 订单金额统计偶尔不准
  • 某个接口 RT 突然从 50ms 飙到 5s
  • CPU 不高,但请求就是卡死
  • 线程数一路上涨,最后 OOM
  • 明明加了 synchronized,结果还是线程不安全

这些现象背后,往往不是单点 bug,而是对并发模型理解不完整:可见性、原子性、有序性、锁竞争、线程池策略、容器线程安全、死锁、上下文切换等问题交织在一起。

这篇文章不打算泛泛讲概念,而是按“现象 -> 定位 -> 修复”的 troubleshooting 方式,拆 8 个 Java 开发中最常见的并发坑,并给出可运行代码和排查路径。


核心原理

并发排查之前,先把三个关键词钉牢:

1. 原子性

一个操作要么全部完成,要么完全不做。
比如 count++ 看起来是一行,实际上通常分三步:

  1. 读取 count
  2. count + 1
  3. 写回 count

多个线程同时做这件事,就会丢失更新。

2. 可见性

一个线程改了共享变量,另一个线程能不能马上看到?
如果没有同步手段,线程可能一直读到旧值。

典型修复手段:

  • volatile
  • synchronized
  • Lock
  • 并发容器
  • 原子类

3. 有序性

编译器和 CPU 可能会做指令重排。
单线程下没问题,多线程下可能触发“看起来不可能”的现象,比如双重检查锁失效。


flowchart TD
    A[并发异常现象] --> B{表现类型}
    B --> C[结果不准]
    B --> D[偶发卡死]
    B --> E[吞吐下降]
    B --> F[线程暴涨]
    C --> G[原子性/可见性问题]
    D --> H[死锁/锁等待/阻塞队列]
    E --> I[锁竞争/线程池配置不当]
    F --> J[线程泄漏/任务堆积]

并发排查的一个通用脑图

我一般按下面这条线排:

  1. 先看现象:数据错了?慢了?卡死?OOM?
  2. 再看线程状态RUNNABLEBLOCKEDWAITINGTIMED_WAITING
  3. 再看共享资源:变量、集合、缓存、连接池、线程池、锁
  4. 最后确认 happens-before 关系:有没有同步保护?

sequenceDiagram
    participant U as 用户请求
    participant T1 as 业务线程A
    participant T2 as 业务线程B
    participant S as 共享资源
    U->>T1: 更新共享变量
    U->>T2: 读取共享变量
    alt 没有同步措施
        T1-->>S: 写入可能延迟可见
        T2-->>S: 读取旧值
    else 使用 volatile/锁/原子类
        T1-->>S: 写入建立可见性
        T2-->>S: 读取最新值
    end

现象复现

下面 8 个问题,我会尽量按“线上最常见的表现”来组织。

  1. count++ 结果不对
  2. volatile 用错,以为能保证线程安全
  3. HashMap 并发写导致异常数据
  4. SimpleDateFormat 多线程下解析错乱
  5. 死锁导致接口卡死
  6. 线程池配置不当,请求堆积
  7. ThreadLocal 使用不当导致内存泄漏或脏数据
  8. 锁粒度过大,吞吐明显下降

实战代码(可运行)

下面这段示例代码把 8 个坑拆成了多个可独立运行的方法。为了便于阅读,我把它们放在一个类里,运行时手动打开对应测试即可。

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;

public class ConcurrentPitfallsDemo {

    // 1. 原子性问题
    private static int unsafeCounter = 0;
    private static AtomicInteger safeCounter = new AtomicInteger(0);

    // 2. volatile 仅保证可见性
    private static volatile int volatileCounter = 0;

    // 3. 非线程安全集合
    private static Map<Integer, Integer> unsafeMap = new HashMap<>();
    private static Map<Integer, Integer> safeMap = new ConcurrentHashMap<>();

    // 4. 非线程安全工具类
    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    // 5. 死锁演示
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    // 7. ThreadLocal
    private static final ThreadLocal<String> context = new ThreadLocal<>();

    // 8. 锁粒度问题
    private static final ReentrantLock bigLock = new ReentrantLock();
    private static final ConcurrentHashMap<String, AtomicInteger> segmentedCounter = new ConcurrentHashMap<>();

    public static void main(String[] args) throws Exception {
        testAtomicityProblem();
        testVolatileMisuse();
        testHashMapProblem();
        testSimpleDateFormatProblem();
        testDeadlock();
        testThreadPoolProblem();
        testThreadLocalProblem();
        testLockGranularity();
    }

    // 1) count++ 丢失更新
    public static void testAtomicityProblem() throws InterruptedException {
        unsafeCounter = 0;
        safeCounter.set(0);

        int threadCount = 20;
        int loop = 10000;
        CountDownLatch latch = new CountDownLatch(threadCount);
        ExecutorService pool = Executors.newFixedThreadPool(threadCount);

        for (int i = 0; i < threadCount; i++) {
            pool.execute(() -> {
                for (int j = 0; j < loop; j++) {
                    unsafeCounter++;
                    safeCounter.incrementAndGet();
                }
                latch.countDown();
            });
        }

        latch.await();
        pool.shutdown();

        System.out.println("期望值: " + (threadCount * loop));
        System.out.println("unsafeCounter: " + unsafeCounter);
        System.out.println("safeCounter: " + safeCounter.get());
    }

    // 2) volatile 不能保证复合操作原子性
    public static void testVolatileMisuse() throws InterruptedException {
        volatileCounter = 0;
        int threadCount = 10;
        int loop = 10000;
        CountDownLatch latch = new CountDownLatch(threadCount);
        ExecutorService pool = Executors.newFixedThreadPool(threadCount);

        for (int i = 0; i < threadCount; i++) {
            pool.execute(() -> {
                for (int j = 0; j < loop; j++) {
                    volatileCounter++;
                }
                latch.countDown();
            });
        }

        latch.await();
        pool.shutdown();

        System.out.println("volatileCounter 期望值: " + (threadCount * loop));
        System.out.println("volatileCounter 实际值: " + volatileCounter);
    }

    // 3) HashMap 并发写
    public static void testHashMapProblem() throws InterruptedException {
        unsafeMap.clear();
        safeMap.clear();

        int threadCount = 20;
        int loop = 1000;
        CountDownLatch latch = new CountDownLatch(threadCount);
        ExecutorService pool = Executors.newFixedThreadPool(threadCount);

        for (int i = 0; i < threadCount; i++) {
            final int threadNo = i;
            pool.execute(() -> {
                for (int j = 0; j < loop; j++) {
                    int key = threadNo * loop + j;
                    unsafeMap.put(key, j);
                    safeMap.put(key, j);
                }
                latch.countDown();
            });
        }

        latch.await();
        pool.shutdown();

        System.out.println("unsafeMap size: " + unsafeMap.size());
        System.out.println("safeMap size: " + safeMap.size());
    }

    // 4) SimpleDateFormat 并发问题
    public static void testSimpleDateFormatProblem() throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(20);
        CountDownLatch latch = new CountDownLatch(100);

        for (int i = 0; i < 100; i++) {
            pool.execute(() -> {
                try {
                    Date date = sdf.parse("2023-06-27 12:34:56");
                    String formatted = sdf.format(date);
                    if (!"2023-06-27 12:34:56".equals(formatted)) {
                        System.out.println("格式化异常: " + formatted);
                    }
                } catch (ParseException e) {
                    System.out.println("解析异常: " + e.getMessage());
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await();
        pool.shutdown();
    }

    // 5) 死锁
    public static void testDeadlock() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (lockA) {
                sleep(100);
                synchronized (lockB) {
                    System.out.println("t1 done");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lockB) {
                sleep(100);
                synchronized (lockA) {
                    System.out.println("t2 done");
                }
            }
        });

        t1.start();
        t2.start();

        Thread.sleep(500);
        System.out.println("如果程序卡住,可能发生了死锁");
    }

    // 6) 线程池配置不当
    public static void testThreadPoolProblem() throws InterruptedException {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2,
                2,
                60,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(2),
                new ThreadPoolExecutor.AbortPolicy()
        );

        for (int i = 0; i < 10; i++) {
            final int taskNo = i;
            try {
                executor.execute(() -> {
                    System.out.println("task " + taskNo + " start");
                    sleep(1000);
                    System.out.println("task " + taskNo + " end");
                });
            } catch (RejectedExecutionException e) {
                System.out.println("task " + taskNo + " 被拒绝");
            }
        }

        executor.shutdown();
        executor.awaitTermination(10, TimeUnit.SECONDS);
    }

    // 7) ThreadLocal 未清理
    public static void testThreadLocalProblem() throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(1);

        pool.execute(() -> {
            context.set("userA");
            System.out.println("第一次请求设置: " + context.get());
            // 故意不 remove
        });

        Thread.sleep(500);

        pool.execute(() -> {
            System.out.println("第二次请求读取: " + context.get());
            context.remove();
        });

        pool.shutdown();
        pool.awaitTermination(5, TimeUnit.SECONDS);
    }

    // 8) 锁粒度过大
    public static void testLockGranularity() throws InterruptedException {
        int threadCount = 10;
        int loop = 10000;
        CountDownLatch latch = new CountDownLatch(threadCount);
        ExecutorService pool = Executors.newFixedThreadPool(threadCount);

        long start = System.currentTimeMillis();

        for (int i = 0; i < threadCount; i++) {
            final int threadNo = i;
            pool.execute(() -> {
                for (int j = 0; j < loop; j++) {
                    String key = "k" + (threadNo % 4);

                    // 大锁,所有 key 竞争同一把锁
                    bigLock.lock();
                    try {
                        segmentedCounter.computeIfAbsent(key, k -> new AtomicInteger()).incrementAndGet();
                    } finally {
                        bigLock.unlock();
                    }
                }
                latch.countDown();
            });
        }

        latch.await();
        pool.shutdown();

        long cost = System.currentTimeMillis() - start;
        System.out.println("大锁耗时(ms): " + cost);
        System.out.println("结果: " + segmentedCounter);
    }

    private static void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException ignored) {
        }
    }
}

8个高频并发问题的排查思路与修复方案

1. count++ 结果不对:典型原子性缺失

现象

  • 计数器偏小
  • 请求量越大,误差越明显
  • 本地单线程测试完全正常

为什么会这样

count++ 不是原子操作,多线程同时更新时会覆盖彼此的结果。

定位路径

先看以下代码有没有共享变量的复合操作:

  • ++
  • --
  • put if absent 的手工实现
  • if (x == null) x = new ...
  • 余额扣减、库存扣减

如果日志里只有“最终值不对”,但没有报错,这类问题概率很高。

修复方案

按场景选:

  • 简单计数:AtomicInteger
  • 高并发热点计数:LongAdder
  • 需要复合一致性:synchronized / Lock

修复示例

private final AtomicInteger counter = new AtomicInteger();

public void incr() {
    counter.incrementAndGet();
}

2. volatile 用错:能保证可见性,不保证复合操作安全

现象

  • 变量最新值能看见,但统计仍然不准
  • 开发同学说:“我都加 volatile 了怎么还不行?”

问题根源

volatile 解决的是:

  • 一个线程写,其他线程可见
  • 禁止部分重排序

但它不保证多个线程同时 ++ 的原子性

常见误区

错误写法:

private volatile int count = 0;

public void incr() {
    count++;
}

正确边界

适合 volatile 的典型场景:

  • 停机标志位
  • 配置开关
  • 单次写、多次读状态位

例如:

private volatile boolean running = true;

public void shutdown() {
    running = false;
}

public void work() {
    while (running) {
        // do something
    }
}

3. HashMap 并发写:数据丢失、覆盖甚至结构异常

现象

  • size 不准
  • 数据偶发丢失
  • 遍历时报错
  • 压测时问题明显,开发环境不稳定复现

为什么是坑

HashMap 不是线程安全的。多个线程同时写同一个 HashMap,结果不可预期。

排查方法

重点搜这些位置:

  • static Map
  • 缓存容器
  • 单例服务里的成员变量
  • List / Set / Map 作为共享对象

修复方案

  • 并发读写:ConcurrentHashMap
  • 读多写少:不可变对象 + 原子替换
  • 复合操作:用 computeIfAbsent 等原子 API

推荐写法

private final ConcurrentHashMap<String, Integer> cache = new ConcurrentHashMap<>();

public void put(String key, Integer value) {
    cache.put(key, value);
}

更进一步,避免“先查再放”的竞态:

cache.computeIfAbsent("k1", k -> 1);

4. SimpleDateFormat 线程不安全:老项目里非常常见

现象

  • 时间解析偶尔异常
  • 格式化结果错乱
  • 很难稳定复现

根因

SimpleDateFormat 内部维护可变状态,多线程复用同一个实例会互相污染。

排查建议

全局搜:

  • static final SimpleDateFormat
  • Spring 单例 Bean 里注入的 SimpleDateFormat

修复方案

优先使用 Java 8+ 的 DateTimeFormatter,它是线程安全的。

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class SafeDateDemo {
    private static final DateTimeFormatter FMT =
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        String s = LocalDateTime.now().format(FMT);
        System.out.println(s);
    }
}

止血方案

如果短期内改动有限,可以:

  • 每次 new 一个 SimpleDateFormat
  • 或者用 ThreadLocal<SimpleDateFormat>

但长期建议直接迁移到 java.time


5. 死锁:线程都活着,但业务像“挂了”

现象

  • 接口一直不返回
  • CPU 不一定高
  • 线程池线程都卡着
  • 应用没崩,但就是处理不过来

常见场景

  • 锁顺序不一致
  • 嵌套锁
  • 数据库锁 + Java 锁混合等待
  • 多资源申请没有统一顺序

定位路径

线上最实用的两个动作:

  1. jstack <pid>
  2. 看线程 dump 中是否出现 Found one Java-level deadlock

死锁关系图

flowchart LR
    T1[线程1] -->|持有| A[锁A]
    T1 -->|等待| B[锁B]
    T2[线程2] -->|持有| B
    T2 -->|等待| A

修复方案

  • 所有线程按统一顺序加锁
  • 尽量避免嵌套锁
  • tryLock + 超时 降低永久等待风险

示例:

ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();

public void safeMethod() throws InterruptedException {
    if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
        try {
            if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
                try {
                    // do work
                } finally {
                    lock2.unlock();
                }
            }
        } finally {
            lock1.unlock();
        }
    }
}

6. 线程池配置不当:不是越大越好

现象

  • 请求高峰时 RT 急剧上升
  • 任务堆积
  • 拒绝策略频繁触发
  • 内存上涨,队列越积越多

常见错误

错误一:直接用 Executors.newFixedThreadPool

它默认使用无界队列,流量打进来时可能一直堆任务,最终把内存拖垮。

错误二:核心线程太少,队列太大

表现是:

  • 线程没打满
  • 任务已经排很长队
  • 用户感知很慢

错误三:CPU 密集和 IO 密集用同一套参数

这类问题特别常见。

排查指标

重点看:

  • activeCount
  • poolSize
  • queue.size()
  • 任务平均执行时间
  • 拒绝次数
  • 最大线程数是否触顶

修复建议

自己显式构造线程池,不要偷懒:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
        8,
        16,
        60,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(200),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.CallerRunsPolicy()
);

参数边界

  • CPU 密集:线程数接近 CPU 核数
  • IO 密集:线程数可以更高,但要看外部资源承载能力
  • 队列不能无限大
  • 拒绝策略要结合业务兜底

7. ThreadLocal 用完不清理:线程复用后数据串请求

现象

  • 用户 A 的上下文跑到用户 B 请求里
  • 某些线程本地变量长期不释放
  • 在线程池场景下问题明显

为什么会这样

ThreadLocal 的值挂在线程对象上。线程池线程会被复用,如果你不 remove(),下一个任务可能读到上一次残留的数据。

定位路径

重点看:

  • 用户上下文
  • TraceId
  • 租户信息
  • 数据源切换
  • 权限上下文

修复方案

必须在 finally 中清理:

public void process(String userId) {
    try {
        context.set(userId);
        // do work
    } finally {
        context.remove();
    }
}

一条经验

如果项目里 ThreadLocal 很多,但没有统一过滤器、拦截器、AOP 清理,那基本迟早出问题。


8. 锁粒度过大:功能对了,性能却崩了

现象

  • 没有错误,但吞吐很差
  • 线程大量 BLOCKED
  • CPU 利用率不高,响应却变慢

本质问题

大家都抢同一把锁,串行化太严重。
最常见的写法就是“大而全”同步块,把本可并行的逻辑全包起来。

典型反例

public synchronized void update(String key) {
    // 所有 key 共用同一把锁
}

优化思路

  • 缩小临界区
  • 分段锁
  • 使用并发容器
  • 读写分离时考虑 ReadWriteLock
  • 避免在锁内做 IO、RPC、数据库操作

状态变化示意

stateDiagram-v2
    [*] --> Running
    Running --> Blocked: 竞争大锁
    Blocked --> Running: 获取到锁
    Running --> Waiting: 条件等待/阻塞调用
    Waiting --> Running: 被唤醒
    Running --> [*]

一个很实用的判断标准

如果锁保护的代码里包含以下任意一种,基本就该警觉了:

  • 远程调用
  • SQL 查询
  • 文件操作
  • 大对象创建
  • 复杂循环

常见坑与排查

这部分我尽量给一条可落地的排查清单,线上救火时很好用。

1. 先看线程栈,不要先猜

常用命令:

jstack <pid> > thread_dump.txt

看重点:

  • 大量 BLOCKED:锁竞争严重
  • 大量 WAITING:可能在等队列、条件变量、Future
  • 大量 TIMED_WAITING:可能 sleep、poll、超时等待
  • 少数线程一直卡在相同代码行:高概率热点锁或死锁

2. 对“共享变量”做全文搜索

全局搜这些关键词很有用:

static
HashMap
ArrayList
SimpleDateFormat
ThreadLocal
synchronized
lock()
volatile

尤其注意单例 Bean 的成员变量,这里最容易藏线程安全问题。

3. 区分“功能正确”和“并发正确”

很多代码在单线程测试里是对的,但并发下不成立。
例如:

if (!map.containsKey(key)) {
    map.put(key, value);
}

这是典型的 check-then-act 竞态条件。

应该改成:

map.putIfAbsent(key, value);

或者:

map.computeIfAbsent(key, k -> value);

4. 不要只盯代码,也要看运行时配置

比如线程池问题,代码可能没错,错在配置:

  • 核心线程数太小
  • 队列太大
  • 拒绝策略不合理
  • 下游依赖慢导致线程被占满

5. 复现时尽量加压,不要只跑一两次

并发 bug 很吃时序。复现技巧:

  • 提高线程数
  • 加循环次数
  • CountDownLatch 对齐起跑时间
  • 多跑几轮
  • 把日志打在线程名、对象 id、锁对象上

定位路径

如果你在线上遇到“偶发数据错乱或接口卡住”,我建议按这个顺序走:

第一步:判断是“错”还是“慢”

  • 结果错:优先查原子性、可见性、线程不安全容器
  • 响应慢/卡死:优先查锁、线程池、死锁、外部依赖阻塞

第二步:看线程池和线程状态

重点回答几个问题:

  • 是线程不够,还是线程都在等?
  • 是队列积压,还是任务执行太慢?
  • 是锁竞争,还是外部 IO 阻塞?

第三步:缩小共享资源范围

列出所有可能被多线程同时访问的对象:

  • 全局缓存
  • 单例 Bean 成员变量
  • 工具类静态对象
  • 上下文变量
  • 连接池/线程池/阻塞队列

第四步:验证修复是否真的生效

不要“改完看着没报错就上线”。
至少做这几件事:

  • 压测复现前后的差异
  • 观察错误率和 RT
  • 看线程数、队列长度、GC
  • 做多轮验证,避免只碰巧没复现

止血方案

有些并发问题线上已经在炸,先止血,再彻底修。

可快速止血的办法

1. 降并发

  • 限流
  • 熔断
  • 降级
  • 缩短调用链

2. 临时串行化关键路径

比如库存扣减、余额变更,先用粗锁保正确,再优化性能。

3. 缩小线程池入口流量

如果任务堆积明显:

  • 调小入口 QPS
  • 启用拒绝保护
  • 快速失败,避免系统雪崩

4. 关闭有风险的异步任务

很多事故不是主流程炸,而是异步消费者无限堆积把系统拖死。


安全/性能最佳实践

1. 优先使用成熟并发工具,而不是手写同步

优先级通常是:

  • java.util.concurrent
  • 原子类
  • 并发容器
  • CompletableFuture
  • 显式锁

少写“自己觉得线程安全”的逻辑。

2. 共享可变状态越少越好

这是最根本的办法。
如果能做到:

  • 无状态服务
  • 方法内局部变量
  • 不可变对象

很多并发 bug 会直接消失。

3. 锁内不要做慢操作

锁内做这些最危险:

  • RPC
  • SQL
  • HTTP 调用
  • 日志大量拼接
  • 复杂对象序列化

4. 线程池要显式命名、显式配置、显式监控

建议至少监控:

  • 活跃线程数
  • 队列长度
  • 拒绝次数
  • 任务执行耗时
  • 最大线程利用率

5. 对上下文类数据统一收口

ThreadLocal、TraceId、租户信息,最好由:

  • Filter
  • Interceptor
  • AOP

统一设置与清理,不要散落在业务代码里。

6. 选择正确的数据结构

不要拿 HashMapArrayList 去硬扛并发。
也不要所有场景都上重锁。先看场景:

  • 高并发计数:LongAdder
  • 并发字典:ConcurrentHashMap
  • 生产消费:BlockingQueue
  • 读多写少:不可变 + 复制替换

7. 建立并发代码的 Code Review 清单

我比较推荐团队固定检查这几项:

  • 有没有共享可变状态?
  • 有没有复合操作未加保护?
  • 容器是否线程安全?
  • 线程池是否有边界?
  • ThreadLocal 是否清理?
  • 锁顺序是否一致?
  • 锁内是否做了慢操作?

总结

Java 并发问题真正难的,不是 API 多,而是表面现象和真正根因往往隔着一层甚至几层。一个“统计不准”,可能是 count++;一个“接口偶发超时”,可能是死锁;一个“内存慢慢涨”,可能是线程池无界队列,也可能是 ThreadLocal 没清理。

这篇文章拆了 8 个高频坑,核心可以浓缩成一句话:

先判断是原子性、可见性、锁竞争还是资源配置问题,再沿着线程状态和共享资源去定位。

如果你让我给最实用的建议,我会给这 4 条:

  1. 共享可变状态能少就少
  2. 并发容器和原子类优先于手写同步
  3. 线程池一定要有边界
  4. 出了并发问题先看线程栈,不要靠猜

最后补一个边界条件:
不是所有并发问题都该用“更重的锁”解决。对于资金、库存这类强一致场景,先保正确;对于统计、监控这类弱一致场景,可以换吞吐更高的方案。修复并发 bug,永远要把业务一致性要求一起带上看。

如果你现在就想在项目里做一次快速自检,最值得先查的地方就是:

  • 单例 Bean 成员变量
  • static 集合与工具类
  • 自定义线程池
  • ThreadLocal
  • 共享缓存与计数逻辑

这几个地方,往往一查一个准。


分享到:

上一篇
《从单体到集群:中级工程师实现高可用服务架构的拆分、负载均衡与故障转移实战》
下一篇
《Kubernetes 集群高可用架构实战:从控制平面冗余到故障切换设计》