news 2026/9/23 9:49:15

告别面试哑火:一个亿小目标带你从入门到精通搞定并发计数

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
告别面试哑火:一个亿小目标带你从入门到精通搞定并发计数

告别面试哑火:一个亿小目标带你从入门到精通搞定并发计数

面试被问原理答不上来,这种尴尬你经历过吗?

当面试官追问“如何准确统计一个亿次请求”时,你只记得 count++,却卡在高并发下的数据丢失上,这直接暴露了基础不牢。

想从入门到精通搞定这类高频考点,光背八股文没用,必须动手跑通一个亿级别的真实场景。

项目目标与痛点拆解

很多初学者以为,统计数量就是简单的加法。但在高并发环境下,这种想法是致命的。

我们要实现的目标很明确:在多线程环境下,准确无误地统计从 0 到 100,000,000 的每一次自增操作。

为什么定这个数?因为 1 亿是一个量级分界线。

低于 1 万,普通变量勉强够用;低于 100 万,原子类开始显现优势;一旦超过 100 万,普通的原子操作会因为缓存行竞争(Cache Line Contention)导致性能断崖式下跌。

这时候,就需要引入分段计数思想。这也是大厂面试中区分“背题选手”和“实战选手”的关键分水岭。

如果只会用 AtomicInteger,面试官会觉得你停留在初级水平。

如果能结合 LongAdder 或者自研的分段锁策略,并解释清楚背后的硬件原理,你就跨过了“入门”的门槛,真正触及了“精通”的边界。

我们的项目将基于 Java 17 实现,理由很简单:它是目前企业级开发的主流版本,且包含了许多性能优化的新特性。

目录结构设计

为了保持代码的可维护性和可扩展性,我们采用标准的 Maven 项目结构。

不要把所有代码都塞在一个 Main 类里,那是玩具代码,不是工程代码。

billions-counter/
├── pom.xml
├── src/
│   └── main/
│       └── java/
│           └── com/
│               └── example/
│                   └── counter/
│                       ├── Main.java
│                       ├── strategies/
│                       │   ├── SimpleCounter.java
│                       │   ├── AtomicCounter.java
│                       │   └── SegmentedCounter.java
│                       └── utils/
│                           └── BenchmarkUtil.java

strategies是核心,里面存放三种不同的计数策略。

utils用于封装通用的基准测试工具,比如启动线程池、等待线程结束、计算耗时等。

这种结构的好处是,后续如果想增加新的策略(比如基于 Redis 的分布式计数),只需新增一个实现类,无需修改主流程代码。

这符合开闭原则,也是代码工程中化的基础。

pom.xml 中,我们不需要引入复杂的第三方库。

Java 标准库中的 java.util.concurrent 包已经足够强大。

唯一需要注意的是,确保编译级别设置为 Java 17,以支持最新的语法特性。

核心代码实现

1. 朴素实现:为什么它必挂?

先看最基础的 SimpleCounter

package com.example.counter.strategies;public class SimpleCounter {private long count = 0;public void increment() {count++;}public long getCount() {return count;}
}

这段代码看起来毫无问题,但在并发环境下,count++ 并非原子操作。

它被编译成三条字节码指令:读取 count 值、加 1、写回 count

如果线程 A 读取了值,线程 B 也读取了同样的值,两者各自加 1 后写回,最终结果只增加了 1,而不是 2。

这就是经典的“丢失更新”问题。

在单线程测试中,它永远正确。但在高并发下,误差会非常大。

2. 原子实现:锁的代价

接下来看 AtomicCounter

package com.example.counter.strategies;import java.util.concurrent.atomic.AtomicLong;public class AtomicCounter {private final AtomicLong counter = new AtomicLong(0);public void increment() {counter.incrementAndGet();}public long getCount() {return counter.get();}
}

AtomicLong 使用了 CAS(Compare-And-Swap)机制。

底层依赖 CPU 的原子指令,保证操作的原子性。

但这带来了新的问题:自旋锁

当多个线程竞争同一个内存地址时,如果 CAS 失败,线程不会阻塞,而是不断重试(自旋)。

在高并发场景下,这种自旋会消耗大量的 CPU 周期。

根据 Oracle 官方开发者文档的建议,在高竞争场景下,AtomicLong 的性能会显著低于预期。

3. 分段实现:真正的性能王者

为了解决竞争问题,我们引入 SegmentedCounter

核心思想:化整为零

不把所有请求都打到同一个计数器上,而是分散到多个独立的计数器上。

最后统计时,再将各个分段的结果累加。

package com.example.counter.strategies;import java.util.concurrent.atomic.LongAdder;public class SegmentedCounter {// 分段数量,通常为 CPU 核心数的倍数private static final int SEGMENT_COUNT = 32;private final LongAdder[] segments = new LongAdder[SEGMENT_COUNT];public SegmentedCounter() {for (int i = 0; i < SEGMENT_COUNT; i++) {segments[i] = new LongAdder();}}public void increment() {// 使用线程 ID 的哈希值决定落到哪个分段int index = (int) (Thread.currentThread().getId() % SEGMENT_COUNT);segments[index].increment();}public long getCount() {long sum = 0;for (LongAdder segment : segments) {sum += segment.sum();}return sum;}
}

这里我们直接使用了 JDK 提供的 LongAdder

为什么不用 AtomicLong

LongAdder 内部就是采用了分段技术(Cell 数组)。

它的设计哲学是:牺牲实时性,换取吞吐量

increment() 时,它并不保证立即看到最新值,但在高并发下,它的性能远超 AtomicLong

对于“统计一个亿”这种场景,我们更关心最终结果的准确性,而不是每次调用后立即读取最新值。

4. 基准测试工具

为了量化对比,我们编写一个测试工具。

package com.example.counter.utils;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;public class BenchmarkUtil {public static void runBenchmark(Runnable task, int threadCount, long iterationsPerThread) throws InterruptedException {ExecutorService executor = Executors.newFixedThreadPool(threadCount);AtomicBoolean done = new AtomicBoolean(false);long startTime = System.nanoTime();for (int i = 0; i < threadCount; i++) {executor.submit(() -> {for (long j = 0; j < iterationsPerThread; j++) {task.run();}});}// 等待所有任务完成executor.shutdown();while (!executor.awaitTermination(1, TimeUnit.HOURS)) {// 超时处理}long endTime = System.nanoTime();double durationMs = (endTime - startTime) / 1_000_000.0;System.out.printf("耗时: %.2f 毫秒%n", durationMs);}
}

注意:这里使用了 awaitTermination 来确保所有线程执行完毕后再计算耗时。

这是性能测试中最容易出错的地方之一。

运行与测试

现在,我们来跑一下这三个策略。

假设我们启动 8 个线程,每个线程执行 12,500,000 次自增,总计 1 亿次。

测试 1:SimpleCounter

SimpleCounter simple = new SimpleCounter();
BenchmarkUtil.runBenchmark(simple::increment, 8, 12_500_000);
System.out.println("结果: " + simple.getCount());

结果预测:耗时极短,但结果远小于 1 亿。

实际运行结果可能在 8,000 万到 9,500 万之间波动,具体取决于 CPU 调度和竞争激烈程度。

结论:数据丢失严重,不可用于生产环境。

测试 2:AtomicCounter

AtomicCounter atomic = new AtomicCounter();
BenchmarkUtil.runBenchmark(atomic::increment, 8, 12_500_000);
System.out.println("结果: " + atomic.getCount());

结果预测:结果精确为 100,000,000。

耗时:在高并发下,耗时较长。在 8 核机器上,可能需要 500-800 毫秒。

原因:CAS 失败率高,导致大量自旋等待。

测试 3:SegmentedCounter

SegmentedCounter segmented = new SegmentedCounter();
BenchmarkUtil.runBenchmark(segmented::increment, 8, 12_500_000);
System.out.println("结果: " + segmented.getCount());

结果预测:结果精确为 100,000,000。

耗时:显著降低。在相同环境下,耗时可能在 100-200 毫秒之间。

性能提升:相比 AtomicCounter,吞吐量提升 3-5 倍。

这就是分段计数的威力。

它通过减少锁竞争(实际上是减少内存地址的写冲突),极大地提高了并发处理能力。

优化扩展与避坑指南

在实际工程中,还有几个关键点需要注意。

1. 分段数量的选择

SegmentedCounter 中,我们将分段数设为 32。

这个值不是随便定的。

建议参考 JDK 开发者文档 中关于 LongAdder 的说明:Cell 数组的大小通常会根据 CPU 核心数动态调整。

一般建议分段数为 CPU 核心数的 2-4 倍。

如果分段太少,竞争依然存在;如果分段太多,最后汇总时的遍历成本会增加。

对于单核机器,分段数设为 1 即可;对于 8 核机器,32 是一个不错的起点。

2. 读取一致性问题

LongAddersum() 方法并不保证强一致性。

如果在统计过程中,其他线程仍在写入,sum() 返回的值可能不是最新的。

在“统计一个亿”这种场景下,通常会在所有写入操作结束后再调用 sum(),因此这个问题影响不大。

但如果需要实时监控计数,就需要权衡实时性和性能。

3. 内存屏障

AtomicLongLongAdder 内部都使用了 volatile 语义或内存屏障。

这意味着,它们不仅保证了原子性,还保证了可见性。

不要试图通过手动添加 synchronized 来“优化”这些类,那只会适得其反。

4. 跨语言对比

如果你熟悉 Go 语言,会发现 sync/atomic 包中的 AddInt64 行为类似 AtomicLong

但 Go 1.21 之后,引入了更高效的 atomic.Int64 类型,其底层实现也借鉴了分段思想。

不同语言在并发原语上的演进方向是一致的:从粗粒度锁到细粒度原子操作,再到无锁或分段无锁结构。

小结

SimpleCounter 的丢数据,到 AtomicCounter 的性能瓶颈,再到 SegmentedCounter 的高吞吐,我们完成了一个从入门到精通的认知闭环。

面试中,如果只答出 AtomicLong,说明你只知道“是什么”。

如果能解释清楚“为什么 LongAdder 在高并发下更快”,并画出分段计数的内存模型,说明你理解了“为什么”。

这种深度,才是企业级开发所看重的。

技术没有银弹,只有最适合场景的方案。

在低并发、强一致性要求高的场景,AtomicLong 依然是首选。

在高并发、最终一致性可接受的场景,LongAdder 或自研分段计数器才是正解。

记住,性能优化的核心永远是:减少竞争,降低延迟,提高吞吐。

这个“一个亿小目标”的项目,看似简单,实则涵盖了并发编程的核心难点。

建议你亲自跑一遍代码,修改线程数和分段数,观察性能变化。

纸上得来终觉浅,绝知此事要躬行。

还有什么不懂的?评论区留言挨个回。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 9:48:58

西门子伺服驱动器版本升级API全变?手写实现兼容层实战

西门子伺服驱动器版本升级API全变?手写实现兼容层实战 刚把项目里的西门子伺服驱动器从 S120 升级到 S120 新版固件,代码一跑全崩?别慌,这坑太常见了。很多开发者以为只是参数变个名,结果发现通信协议里的功能码定义完全重构,旧的 Modbus 指令直接报 Exception: Unknown…

作者头像 李华
网站建设 2026/9/23 9:48:49

滑动门代码避坑指南:3个配置陷阱让项目秒级响应

滑动门代码避坑指南:3个配置陷阱让项目秒级响应 刚接手老项目时,配置滑动窗口限流器卡了我整整半天。明明照着文档抄代码,上线后要么内存溢出,要么并发量一高就死锁。直到翻遍源码发现,大家最容易踩的三个坑全在初始化参数和线程安全上。这篇 避坑指南 不聊虚的,直接拆解 guava…

作者头像 李华
网站建设 2026/9/23 9:48:46

图解中国彩王底层逻辑 3步搞定配置环境卡点

图解中国彩王底层逻辑 3步搞定配置环境卡点 配置环境就卡半天,是不是你的常态?明明照着文档一步步来,依赖装了一堆,服务起不来,报错信息长得像天书。别急,这不是你笨,是没人把【中国彩王】这套系统的【图解原理】给你掰开了揉碎了讲。今天不聊虚的,直接上干货,用大白话+代码,带你穿透表象,看清它到底是怎么跑…

作者头像 李华
网站建设 2026/9/23 9:48:42

3天搞定adsl调制解调器配置,这份保姆级教程救了我的命

3天搞定adsl调制解调器配置,这份保姆级教程救了我的命 配置环境就卡半天,这大概是每个刚接手老旧网络项目工程师的噩梦。你盯着那台布满灰尘的adsl调制解调器,看着路由器上疯狂闪烁的红灯,心里只有一句话:这玩意儿到底怎么连?别急,今天这篇保姆级教程,不讲虚的,直接带你从零搭建一个能跑的adsl拨号调…

作者头像 李华
网站建设 2026/9/23 9:48:29

搞懂新能源产业有哪些,手写实现数据看板提速3倍

搞懂新能源产业有哪些,手写实现数据看板提速3倍 刚写完业务逻辑,感觉代码跑通了,心里一松?别急着庆祝。你发现没,页面一刷,数据卡得跟老牛拉破车似的?这就是典型的 学会语法却不知怎么搭项目 的陷阱。很多人对着教程敲代码,能跑就行,结果上线后用户骂娘。 今天咱们不聊虚的,就聊 新能源产业有哪些…

作者头像 李华
网站建设 2026/9/23 9:48:24

5分钟搞定必死陷阱:Python与Go进程控制完整示例对比

5分钟搞定必死陷阱:Python与Go进程控制完整示例对比 官方文档翻了三遍还是晕头转向?别急,直接上干货。很多老铁在搞自动化运维或者后端服务时,卡在进程管理的“必死”问题上,其实就是没看懂 完整示例…

作者头像 李华