news 2026/9/30 4:39:39

Java高并发计数核心:原子类、CAS与LongAdder实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java高并发计数核心:原子类、CAS与LongAdder实战解析

没有主标题,直接从二级标题开始。

1. 为什么高并发下的计数会“打架”

1.1 一个 i++ 背后藏着的三步操作

做后端开发的朋友应该都对“高并发计数”这个需求不陌生。无论是统计在线人数、记录接口调用次数、生成递增序列号,还是做各种限流和监控,本质上都是同一个模型:多个线程同时对同一个共享变量做“读-改-写”操作。而这个模型里最经典的坑,就是那个看起来人畜无害的i++。

很多人第一步接触并发时都会想:单线程里i++不就是一行代码吗?还能出错?但严格来说,i++在JVM字节码层面根本不是一条指令。它拆开大概是三步:先读取i的当前值,然后把这个值加1,最后把结果写回i。如果在多线程环境下,线程A读到了i=5,还没来得及写回6,线程B也读到了5,那A和B各自写回6,最终i只增加了1,真实的两次请求“丢失”了一次计数。这就是典型的丢失更新问题。

我在很多项目里见过这种场景。最典型的是用HashMap或者普通int变量来做缓存命中统计,初始看起来没事,压测一旦把并发喊上去,统计数字就永远对不上账。排查成本还特别高,因为它是偶发的,不是必现的。线程调度稍微错开一点,结果就不同,log里根本看不出规律。所以,搞高并发计数,第一件事就是把“非原子操作”这个认知刻在脑子里。

1.2 从加锁到无锁:三种方案的演进

既然i++不安全,那最早期的做法自然是加锁。原先的写法通常是给整个increase()方法加synchronized,或者用ReentrantLock包住临界区。锁的方案逻辑上绝对正确,问题在于性能——锁的获取、竞争、释放都涉及操作系统层面的上下文切换和线程阻塞唤醒,在高竞争场景下,锁非但没帮上忙,反而变成了瓶颈。用最直白的话说:本来加个数字只要几纳秒,抢一次锁可能要花几微秒,幅度差了两三个数量级。

后来引入了原子类,也就是java.util.concurrent.atomic包下面那一堆类。它们背后的思想完全不同:不需要阻塞线程,而是通过CPU底层的CAS指令,乐观地尝试更新。没有竞争就能直接成功,有竞争就“自旋”重试。这等于把“排队进厕所”改成了“自己看着办,有空位就进,没空位就再试一次”,吞吐量和延迟表现都好很多。

到了更高并发阶段,还演化出了LongAdder这样的“分段计数”方案。它把原来一个共享热点变量拆成多个Cell单元,各个线程分散到不同单元里加,最后求和时再合并。这个思路跟ConcurrentHashMap的分段锁是一脉相承的,核心都是降低热点竞争。

1.3 什么时候该用原子类

原子类不是万能的,但有非常明确的适用边界。它适合的是:单变量、操作简单、逻辑独立、且对数据一致性要求不是“强事务性”的场景。比如计数器、状态开关、序列号生成器、统计指标等。反过来,如果你要做的是一组变量必须同时更新,或者某个操作需要先检查再决定,还伴随业务判断,那就不是原子类能搞定的,必须上synchronized、ReentrantLock,甚至分布式锁。

我自己的选型习惯是:能无锁就无锁,但要判断操作组合的复杂度。单飞的操作交给原子类,多个步骤组合的原子性交给锁。这不是偷懒,而是让每一个工具待在它该待的位置。

2. 原子类的心脏:CAS 原理硬核拆解

2.1 CAS 的三个操作数与硬件级支持

CAS,全称 Compare And Swap,翻译成中文就是“比较并交换”。它是无锁并发最底层的基石。CAS操作接收三个参数:内存地址V、预期值A、要写入的新值B。它的执行逻辑是:只有当V当前位置存储的数和A相等时,才把V更新为B;否则什么都不做。

这看起来不就是“先比一比,再决定要不要写”吗?但关键在于“比较”和“交换”这两个动作在硬件层面被设计成了一个不可分割的原子操作。CPU提供了对应的指令,x86平台上是CMPXCHG,ARM平台是LDREX/STREX一族的实现。也就是说,原子性这件事,在CAS这里已经不依赖Java的锁机制,而是直接把宝压在CPU指令上,既不用阻塞线程,也不需要上下文切换。

用一个生活化的比方来解释:你去抢最后一个停车位,系统显示“空余1个”,你点“占用”,但就在你点击的瞬间,另一个人也看到了“空余1个”并抢先点了占用。这时候CAS会怎么处理?它会发现你预期的“空余1个”已经变成了“空余0个”,于是你的操作失败,只能重新加载最新状态再试一次。整个过程你没有被阻塞,不需要拿号排队,只是“失败重试”而已。

2.2 从 Java 到 CPU:一条 compareAndSet 的完整链路

很多刚开始看源码的人,看到AtomicInteger.compareAndSet()内部的unsafe.compareAndSwapInt会一头雾水。这个Unsafe类虽然名字叫“不安全”,但它在JDK内部大量使用。它提供了一些绕过Java内存管理、直接操作内存地址的底层能力。compareAndSwapInt接收对象引用、字段偏移量、预期值、更新值四个参数,然后把这个调用映射到JVM的本地方法,最终执行到CPU的CAS指令。

整条链路是这样的:Java代码调用AtomicInteger.compareAndSet(expect, update),进入AtomicInteger内部,调用Unsafe.compareAndSwapInt,JVM通过JNI(Java Native Interface,Java本地接口)转发到C++实现的底层方法,最后落到lock cmpxchg指令。x86平台上的cmpxchg指令不是天然完全原子的,所以前缀还会加上一个lock来锁住总线或缓存行,确保多核CPU之间的协同不会被干扰。

这条链路清楚之后,很多问题自然就通了。比如为什么说CAS是无锁的——它确实没加Java层面的锁,但底层其实还是需要硬件级别的 “lock” 来保证原子性,只不过这个开销远比操作系统锁小得多。

2.3 自旋与重试:无锁并发的基本动作

CAS操作单次要么成功要么失败,但调用方通常不会“失败就放弃”,而是会循环重试,这个循环过程叫自旋(Spin)。自旋锁的名字也源自于此——线程在循环里“转圈”等待,而不是被挂起。

写一个最简单的自旋实现,底层逻辑就是getAndIncrement的核心:

public static int spinIncrement(AtomicInteger atomic, int times) { int result; do { // 先拿到当前值 result = atomic.get(); // 计算新值,再尝试CAS写入 } while (!atomic.compareAndSet(result, result + 1)); return result; }

do-while会一直执行到某个线程CAS成功才退出。如果竞争不激烈,第一次尝试往往就成功了,开销极小。这也是为什么在低并发场景下,原子类比锁更快的原因——它没有线程切换成本,只做一次CPU比较和写操作。

但自旋也有代价。如果竞争非常激烈,大量线程同时失败、同时重试,CPU就被白白消耗在空转上,核数越多,这种“空转风暴”越明显。所以“无锁”并不等于“零成本”,理解这一层,才能在适当地点选择锁还是CAS。

3. 常用 JUC 原子类逐个击破

3.1 四大基础原子类速查表

java.util.concurrent.atomic包里的类可以按用途大概归成这么几类,后面实战部分我会逐个配场景讲。

原子类类型核心用途典型应用场景
AtomicInteger整型对int做原子更新计数器、序号生成、限流计数
AtomicLong长整型对long做原子更新大数值计数、全局ID段、时间戳
AtomicBoolean布尔型对boolean做原子更新状态开关、幂等标记
AtomicReference<V>引用类型对对象引用做原子更新无锁栈/队列、配置热更新
AtomicIntegerArray整型数组对数组元素做原子更新分片统计、钉扎数据
AtomicLongArray长整型数组对数组元素做原子更新分桶队列指标
AtomicStampedReference<V>引用+版本号带版本号的引用原子更新解决ABA问题、无锁数据结构
LongAdder长整型高并发累加热点统计消息量统计、访问量累计,读少写多
LongAccumulator长整型自定义累加规则自定义聚合计算

每个类都有各自的get/set/compareAndSet/getAndAdd/addAndGet这一类方法,接口风格高度一致。掌握了AtomicInteger,其余基本都是举一反三。

3.2 AtomicInteger 核心 API 使用要点

日常开发里,AtomicInteger是出现频率最高的一个。它的核心方法不多,但对这些方法背后的语义差异要有感知。

addAndGet(int delta)是“加 delta 并返回新值”;getAndAdd(int delta)是“返回旧值再增加 delta”。这里在并发场景下有个容易忽略的点:这两个方法返回的“值”只能代表方法执行那一刻的瞬时快照,不等同于业务上的最终结果。如果 A、B 两个线程同时addAndGet(1),可能A返回1,B返回2,这个顺序本身是确定的,但如果后续业务要依赖这两个返回值做某种“谁先谁后”的判断,那就需要额外设计。

compareAndSet(int expect, int update)是手动控制更新逻辑的关键。比如我们要实现一个“不超过某个上限的计数器”:

public static boolean incrementWithLimit(AtomicInteger counter, int limit) { while (true) { int current = counter.get(); if (current >= limit) { return false; // 已达到上限,放弃 } int next = current + 1; if (counter.compareAndSet(current, next)) { return true; // CAS成功,本次更新有效 } // 如果CAS失败,说明值被其他线程改了,循环再试 } }

这个手动CAS的写法,就是在解决“先检查后操作”的原子性问题。如果用addAndGet再事后判断是否越界,在并发下一定会出现瞬间超限的问题。

lazySet(int newValue)是一个比较冷门但高效的方法。它只保证最终的可见性,不保证顺序性,相比set少了内存屏障的开销。在高吞吐、低即时性要求场景(比如统计上用到的重置)可以考虑它,但常规业务不建议轻易用。

3.3 AtomicReference 与 ABA 克星 AtomicStampedReference

AtomicReference是把CAS从基本类型扩展到了对象引用。它可以用来做无锁的链表、栈,或者实现配置的热更新。但引用类型的CAS有一个著名的问题,就是ABA问题。

什么叫ABA?简单说:线程A读取到变量值是X,然后它在做其他事情的过程中,变量被线程B改成了Y,又被线程C改回了X。等线程A要执行CAS的时候,它发现变量还是X,于是认为“没有被改动过”,更新就成功了。可实际上值被改过一轮了,这在某些场景下会导致隐患。

怎么解决?加版本号。AtomicStampedReference内部持有一个引用对象和一个整型版本号。每次修改时,只要版本号变,CAS就会失败:

AtomicStampedReference<String> ref = new AtomicStampedReference<>("init", 0); // 更新时同时检查引用和版本号 boolean ok = ref.compareAndSet("init", "updated", 0, 1);

这样即使引用值看起来没变,只要版本号对不上,CAS依然会失败。在我做无锁队列和无锁栈的时候,这个类就是“保命”的关键。常规业务计数器很少用到它,但一旦你写底层中间件或者自己封装无锁数据结构,就绕不过去了。

3.4 LongAdder:高竞争场景的终极形态

如果只是看AtomicLong在极低竞争下的性能,它几乎无可挑剔。可当线程数一多,同一个内存地址成为全JVM的热点,所有CAS都往这一个坑里挤,性能就会快速下降。LongAdder的思路是:把这个热点拆开。

LongAdder内部维护一个基准值base和一个Cell[]数组。每个Cell是一小块缓存行对齐的空间。线程做累加时,先通过ThreadLocalRandom算出当前线程的哈希,定位到其中一个Cell上进行CAS累加。这样不同线程落在不同Cell上,把原来单点的竞争分散成了多个点的分散竞争。只有需要sum()的时候,才把base和所有Cell的值加到一起,但sum()本身是不加锁的,属于“最终一致性”的快照。

所以LongAdder特别适合“写多读少”的场景。比如统计平台每秒消息积压量、累计PV、请求次数的累计指标,这类场景根本不在乎中间某个瞬间的准确值,只在乎最终结果。反过来,如果你需要用计数器生成全局唯一ID,那就必须用AtomicLong或者AtomicInteger,因为每一步操作都需要拿到一个精确且全局递增的值,LongAdder给不了这种强一致性。

4. 实战复盘:用原子类解决四个真实计数场景

4.1 场景一:在线人数统计,告别 synchronized

很多项目在初期喜欢用synchronized方法或AtomicInteger来统计在线人数。如果并发低,什么都好说,可一旦秒杀或者活动上线,同时登录/登出的用户一多,锁竞争就会让原本简单的增减操作变得昂贵。

用AtomicInteger重构在线人数统计其实特别简单:

public class OnlineUserCounter { private final AtomicInteger count = new AtomicInteger(0); public void onUserLogin() { count.incrementAndGet(); } public void onUserLogout() { count.decrementAndGet(); } public int currentOnline() { return count.get(); } }

看起来平平无奇,但它去掉了一个潜在的性能地雷。这里提个细节:如果是“先判断是否已登录,再决定是否 +1”这种复合逻辑,单纯incrementAndGet是不够的,必须做CAS自旋,或者配合业务状态字段用锁。我自己写游客登录统计时就踩过这个坑——没做判断直接 ++,同个用户从两个设备进来,在线数瞬间翻了倍。

4.2 场景二:本地唯一序列号分配

订单号、流水号这类需求,在分布式架构下通常由发号器统一生成,但很多时候单机场景也可以用本地序列号过渡。用AtomicLong做本地发号器,是最经典也最可靠的方式之一。

public class SeqGenerator { private final AtomicLong seq = new AtomicLong(0L); public long next() { return seq.incrementAndGet(); } }

再进阶一点,如果发号需要带业务前缀、机房编号和时间戳,一般会组装成一个字符串:

public String nextOrderNo() { long l = seq.incrementAndGet(); return String.format("%s%04d%010d", "NO", bizCode, l); }

这里有个注意点:AtomicLong.incrementAndGet()在达到Long.MAX_VALUE时会溢出为负数。真实业务里几乎不会触到这个上限,但为了防止异常数据,可以在达到某阈值之后做告警,或者设计重置逻辑。别在这块省代码,线上看到负数序列号的时候排查成本远大于提前防御的成本。

4.3 场景三:接口 QPS 实时监控

QPS(每秒请求数)统计是运维排障和容量评估的基础数据。实现思路不复杂:维护一个当前秒的计数器,新来一个请求就加1,同时通过定时任务或者懒刷新方式,每到新秒就重置计数并上报。

用AtomicLong实现一个多线程安全的QPS计数器,比用HashMap<Long, Long>存每秒的累计值要轻量得多。

public class QpsMeter { private final AtomicLong qps = new AtomicLong(0); private volatile long lastSecond = System.currentTimeMillis() / 1000; public void hit() { long currentSecond = System.currentTimeMillis() / 1000; if (currentSecond != lastSecond) { // 新的一秒,清掉旧计数 synchronized (this) { if (currentSecond != lastSecond) { qps.set(0); lastSecond = currentSecond; } } } qps.incrementAndGet(); } public long currentQps() { return qps.get(); } }

这里用了一个synchronized快路径拦截,是因为“跨秒重置”这个动作是典型的复合操作:先判断当前秒和上一秒是否相等,再决定要不要清零。直接用原子类做也是CAS自旋的变体,但重置动作本身就涉及共享状态lastSecond的更新,用一小段同步代码反而更稳妥。这个设计思想值得记住:原子类和锁不是互斥的,把锁的粒度放到最小,大多数路径用无锁,只在极少数跨秒时连接处才用锁,是性能与正确性的均衡。

4.4 场景四:库存扣减为什么不要直接上原子类

原子类在计数类场景里大放异彩,但有一个非常容易踩坑的领域:库存扣减。很多人觉得库存也是数字,扣库存就是decrementAndGet嘛,多简单。但库存和“计数”最大的区别在于,库存扣减涉及“事务边界”——通常还得同时记录订单、锁定用户、记录扣减流水。如果只更新一个库存数字,数据库行锁和事务的一致性就被破坏了。

举个典型的并发秒杀例子:库存剩1件,A、B两个用户同时提交订单,用AtomicInteger扣减库存,能保证只有一个用户扣到,但从业务上看,这个扣减动作发生在事务提交之前还是之后?如果先扣库存后下单,订单创建失败,库存已经飞了;如果先下单后扣库存,又可能出现超卖。所以原子类做不了这件事的全局一致性,必须配合数据库事务和锁策略。

那原子类在库存场景就没用了吗?也不是。它可以用来做“预占式限流”:用一个AtomicInteger初始化成总库存,每次购买前先decrementAndGet,如果返回负数就拒绝。这个方案叫“预扣减”,能快速拦掉一批并发请求,是保护数据库的有效前置措施。但真正的扣减操作最终还是要回到数据库事务里去完成。

5. 性能对决:atomic 与锁的实测对比与选型建议

5.1 JMH 压测结果与对比表

我在本地用JMH(Java Microbenchmark Harness,官方微基准测试工具)对不同方案的计数器做过一组粗测。机器是8核16G的普通开发机,JDK版本17。测试内容都是循环执行一百万次累加操作,对比synchronized、ReentrantLock、AtomicLong、LongAdder四类实现。

实现方式低竞争(2线程)中竞争(8线程)高竞争(32线程)
synchronized较快性能明显下降吞吐很差,线程阻塞增多
ReentrantLock略慢于synchronized与synchronized接近同样受限于阻塞开销
AtomicLong最快吞吐下降,但依然可用自旋导致CPU占用飙升
LongAdder略慢于AtomicLong开始超越AtomicLong优势非常明显,吞吐领先数倍

这个表格只是趋势参考,具体数字跟机器和JVM参数有关,但方向是稳定的。低竞争环境下AtomicLong就是最优解,简单且确定性高;一旦线程数超过CPU核心数的两三倍,竞争激烈程度上来,LongAdder能在吞吐上甩开AtomicLong一个档次。

那个“最快”的结论是不是意味着我们一律用AtomicLong就对了?不是。选型还要看读写的比例。LongAdder的sum()需要遍历所有Cell,如果每次都要拿精确值判断,比如“当前计数必须精确等于5才放行”,那LongAdder反而慢。它适合“最后汇总”而不是“实时精确读取”。

5.2 伪共享(False Sharing)对性能的隐性杀伤

聊性能就绕不开伪共享。CPU缓存是以缓存行(Cache Line)为粒度加载的,x86架构下通常一个缓存行是64字节。如果两个共享变量落在同一个缓存行里,且分别被两个不同核心的线程频繁修改,这两个核心就会不断同步缓存行,导致性能剧烈下降。

LongAdder的高明之处在于,它的Cell对象用@sun.misc.Contended注解做了填充扩展,保证每个Cell独立占满一个缓存行。这就是“以空间换时间”的一个经典工程实现。如果你自己要设计一个“高频写”的多变量结构,可以通过填充字段来避免伪共享:

public class PaddedCounter { public volatile long value = 0; // 填充到接近64字节,避免与其他变量共享缓存行 public long p1, p2, p3, p4, p5, p6, p7; }

这种写法在日常业务代码里用不上,但如果你的中间件被压测时性能上不去,排查时一定别忘了看一下是不是伪共享在捣乱。用perf工具或者JFR热方法分析能看到端倪。

5.3 选型决策流程

经过大量实战,我总结出一个简单好用的选型流程:

  • 如果只是统计一个全局递增或递减的数字,且并发不高:用AtomicInteger/AtomicLong,简单可靠,语义清晰。
  • 如果统计场景是“写多读少”,且读取频率远低于累加频率:用LongAdder。
  • 如果统计需要实时精确值,且并发又很高:不要执着于无锁,考虑分段计数器配合定期汇总。
  • 如果还需要条件更新和对象级别的引用变更:用AtomicReference或AtomicStampedReference。
  • 如果操作涉及多个变量的协同一致性:放弃原子类,直接用锁。

这套流程还不至于套用到所有场景,但覆盖了绝大多数业务使用方向。

6. 常见坑点与面试高频题实战解读

6.1 ABA 问题彻底搞懂

前面提过ABA问题的概念,这里再展开一次,因为它是最经典的原子类“隐藏陷阱”,也是面试几乎必问的考点。

用代码复现一个最简单的ABA场景:

AtomicInteger ai = new AtomicInteger(1); Thread t1 = new Thread(() -> { // 等待t2完成修改 while (ai.get() != 1) { Thread.yield(); } // 此刻值已经是1了,CAS成功 boolean ok = ai.compareAndSet(1, 2); System.out.println("CAS result: " + ok); }); Thread t2 = new Thread(() -> { ai.compareAndSet(1, 5); // 1 -> 5 ai.compareAndSet(5, 1); // 5 -> 1,又变回1了 });

这种“改了一圈又改回来”的过程,在计数器场景下通常是没问题的,因为计数本身只看最终值。但在无锁数据结构里,比如无锁栈,节点A被弹出后又被压入,如果栈顶引用还指向这个对象,CAS就可能把已经被回收的对象误认为“还是原来的栈顶”,导致DAG倒错甚至内存破坏。

解决方案就是加版本号,AtomicStampedReference。每次更新引用时同时更新版本号,任何修改都会导致版本号变化,CAS之前如果发现版本号和预期不匹配,直接判定失败。我在实现无锁栈的时候就是靠这个类避免旧引用复活的问题。

6.2 自旋带来的 CPU 飙升与缓解

原子类自旋在表面上看不到线程阻塞,但这是把双刃剑。高竞争下,每个失败的线程都会立刻进入下一轮CAS,这些“空转指令”虽然耗费极少,但积少成多,会把CPU打满。

我在一个流量监控组件里遇到过这个现象:统计接口QPS用AtomicLong,高峰时段CPU直接从20%飙到95%。排查时先看线程状态,没发现阻塞,再看火焰图,发现热点集中在Unsafe.compareAndSwapLong。原因就是所有统计线程都在抢着更新同一个AtomicLong。

当时把计数数据结构换成了LongAdder,CPU立刻降回40%以内。这就是拆分散热点的价值。如果无法换结构,还可以在自旋循环里加入Thread.yield()或者LockSupport.parkNanos(1)短暂退让,让竞争线程有机会完成更新,而不是大家一起死磕。条件允许时,设置一个自旋次数上限,超过上限就转换策略,也是业界比较成熟的“自适应自旋”思路。

6.3 原子类不能解决“复合操作”的原子性

这是我认为最容易被忽略的边界。原子类只保证了“单次方法调用”的原子性,但并不保证“多方法组合”的原子性。

比如我们有个需求:扣减余额之前必须确认余额大于0。用AtomicLong实现时,如果写两个独立调用——先get()检查,再compareAndSet()扣减——那在检查之后和扣减之前,别的线程可能已经把余额改成负数了。这不是原子类的错,而是我们错误地把“两步操作”当成了“一个操作”。

解决方式要么是把检查和扣减合并到一个CAS自旋循环里,要么就直接用锁包住整个判断和更新过程。这里我强烈建议:当一个操作需要读取多个状态或依赖判断结果时,优先考虑锁。原子类的价值在于单点更新,它不是万能的事务替代品。

6.4 避坑自查清单

最后整理一份自查清单,都是我在实际项目里遇到或复盘过的问题:

现象可能原因排查思路
统计数字明显偏小使用了i++或非原子更新检查字节码或代码,确认是否用了原子类
高并发下CPU飙升大量线程CAS自旋竞争火焰图定位热点,考虑LongAdder或退让策略
CAS明明成功了,值却不符合预期逻辑上使用了两步操作检查是否涉及多个变量的组合更新,改用锁
无锁数据结构偶发数据错乱ABA问题使用AtomicStampedReference加版本号
压测成绩提升不上去伪共享检查热点变量是否在同一缓存行,做填充
计数结果毫秒级不准确LongAdder.sum()不保证强一致确认当前场景是否需要精确值,选择合适类

关于压力测试,多说一句:压测一定要测到线程数超过核心数的场景,不能只在2线程的“舒适区”里验证。并发性能这东西,症状在极端参数下才会显形。

我个人的经验是,原子类本质上是一种“非常贴心但边界分明”的工具。它把并发中最常见的单变量计数问题压缩到极致,几乎不让开发者感知到任何线程同步的存在,但它也有自己的软肋——复合操作、强一致读取、跨变量约束,这些场景还是回归锁和事务。理解了边界,再去看AtomicInteger、LongAdder这些类,就不再是死记API了。做高并发计数,本质上是在“性能”和“一致性”之间反复权衡。无锁不是炫技,锁也不是原罪,用最小代价解决当前问题,才是真正值得追求的目标。

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

Java数组最值查找与返回值设计:从边界处理到泛型封装的完整实践

写这篇内容前&#xff0c;我先交代一下动机。工作中我见过太多为了找个最大值而现场撸循环的代码&#xff0c;不敢说十之八九&#xff0c;但至少半数以上的团队里&#xff0c;做报表、做统计分析、做订单金额校验时&#xff0c;最值查找的逻辑都是散落在各个业务方法里反复复制…

作者头像 李华
网站建设 2026/9/30 4:39:14

程序员持续学习黄金比例:70-20-10法则,告别技术焦虑

这两年我身边越来越多人陷入一种奇怪的状态&#xff1a;一边焦虑技术过时&#xff0c;一边又学不进去&#xff1b;一边收藏一堆“2026必备技术清单”&#xff0c;一边打开文档就犯困。我自己也经历过这个阶段&#xff0c;而且试过不少笨办法&#xff0c;最后才慢慢摸到一点门道…

作者头像 李华
网站建设 2026/9/30 4:39:10

Model-Optimizer模型优化实战:量化、剪枝与蒸馏的流水线设计

1. 模型优化器到底在优化什么第一次接触 Model-Optimizer 这个概念&#xff0c;很多人会下意识把它和“训练优化器”混为一谈。Adam、SGD、AdamW 这些是训练时用来更新梯度的优化器&#xff0c;而 Model-Optimizer 是另一回事——它是在模型训练完成之后&#xff0c;对模型本身…

作者头像 李华
网站建设 2026/9/30 4:38:54

Sqoop离线数据同步全解:从原理到性能调优的实践指南

做数据平台这几年&#xff0c;我处理过最多的需求其实不是复杂的计算&#xff0c;而是“搬数”——把业务库里的订单、用户、流水几类大表搬到HDFS/Hive&#xff0c;或者把数仓算好的结果导回关系库给业务方查询。早期我用JDBC单线程逐条读&#xff0c;一张两千多万行的订单表拉…

作者头像 李华
网站建设 2026/9/30 4:38:01

从二维到三维:ExponentialCosine函数曲面可视化实战解析

做数据可视化这行久了&#xff0c;你会发现一个规律&#xff1a;越是看着简单的东西&#xff0c;想把它讲清楚反而越费劲。比如 ExponentialCosine 这种函数&#xff0c;光看名字挺唬人&#xff0c;但实际上就是指数函数和余弦函数凑在一起。可一旦你把它扔到三维空间里看&…

作者头像 李华
网站建设 2026/9/30 4:37:45

什么是偏光镜?/钟祥极博视科普

一、大晴天开车&#xff0c;眼睛遭罪的滋味咱都懂钟祥的街坊们&#xff0c;不管是自驾去宜昌、武汉跑高速&#xff0c;还是周末拖家带口去莫愁湖、明显陵转转&#xff0c;只要是大晴天&#xff0c;开车上路总有几个瞬间让人眯着眼、皱着眉&#xff1a;路面泛着一层白花花的油光…

作者头像 李华