跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

如何在Java中利用CAS实现非阻塞的计数器限流_AtomicInteger自旋更新与时间窗口结合

AtomicInteger.incrementAndGet() 做限流会失效,因其缺乏时间维度约束,无法自动清零或滑动窗口,导致请求集中爆发或窗口撕裂;应改用时间分片(如秒级窗口)配合 ConcurrentHashMap 与 LongAdder 实现精准限流。 为什么直接用
AtomicInteger.incrementAndGet()
做限流会失效 因为限流不是单纯计数,它需要「时间维度」约束。只靠
AtomicInteger
自增,数值永远涨,没法自动清零或滑动——你看到的“每秒最多100次”,实际可能前10毫秒就打满100次,后面990毫秒完全被拦住,体验断崖式下跌。 常见错误现象:
AtomicInteger
计数器一路飙升,但限流逻辑没生效;或者手动定时重置(比如用
ScheduledExecutorService
每秒清零),结果在清零瞬间出现窗口撕裂:清零前最后一毫秒+清零后第一毫秒,两波请求叠加突破阈值。 别在业务线程里 sleep 等待窗口结束——这等于把非阻塞变成阻塞 别用
System.currentTimeMillis()
做窗口分界——毫秒级精度在高并发下容易误判同一窗口 避免用
new Date()
或
LocalDateTime.now()
,对象创建开销大,且不适用于高频 CAS 场景 用
LongAdder
+ 时间戳分片替代单个
AtomicInteger
LongAdder
比
AtomicInteger
更适合高并发累加,但它本身也不带时间语义。真正关键的是「把时间切片化」:比如按秒级窗口,就用当前秒数(
System.nanoTime() / 1_000_000_000
)做 key,每个窗口独立计数。 实操建议: 立即学习 “ Java免费学习笔记(深入) ”; 用
ConcurrentHashMap
存储「窗口开始时间 → 当前计数」,key 是窗口起始秒的时间戳(如
1717023600L
) 每次请求先算当前窗口:
long window = System.currentTimeMillis() / 1000
,再 get or compute if absent 调用
adder.increment()
,然后立即检查
adder.sum() ,超了就拒绝
不主动清理旧窗口——靠
ConcurrentHashMap
的弱引用或定期扫描
keySet()
过期项(比如只保留最近3个窗口)
compareAndSet()
在限流里基本没用武之地 很多人想用
AtomicInteger.compareAndSet(expected, updated)
实现“仅当未超限时才更新”,但这是错的:CAS 只保证原子性,不保证逻辑正确性。两个线程同时读到 count=99,都判断“99 compareAndSet(99, 100),结果一个成功、一个失败——失败的那个线程已经做了业务判断,却还要兜底处理,反而增加复杂度。 Eclipse导入Android或其他的JAVA项目的正确方法 WORD版 本文档主要讲述的是Eclipse导入Android或其他的JAVA项目的正确方法;希望本文档会给有需要的朋友带来帮助;感兴趣的朋友可以过来看看 下载 更糟的是,如果阈值是动态的,或涉及多个变量(比如“剩余配额 + 时间戳”联合判断),
compareAndSet()
根本无法覆盖。 除非你在写底层无锁数据结构,否则对业务限流来说,
incrementAndGet()
或
add()
配合即时判断更直白可靠
weakCompareAndSet()
和
compareAndSet()
行为差异在 JDK 9+ 已趋同,不用刻意区分,但都绕不开上面的逻辑缺陷 真要强一致性校验,请用
StampedLock
或分离读写路径,而不是硬套 CAS 滑动窗口必须自己算偏移,
AtomicInteger
不支持 固定窗口简单,但抖动大;滑动窗口平滑,可 Java 标准库没有
AtomicSlidingWindow
这种东西。所谓“滑动”,本质是维护一个数组或环形缓冲区,每个槽位存一个时间片的计数,然后每次请求要累加最近 N 个槽位的和。 这时候
AtomicInteger
单个变量完全不够用,必须用数组+索引控制: 定义
AtomicInteger[] slots = new AtomicInteger[windowSize]
,比如 60 个槽代表 60 秒 用
int index = (int)(System.currentTimeMillis() / 1000 % windowSize)
定位当前槽 先
slots[index].incrementAndGet()
,再遍历所有槽 sum —— 但注意:遍历过程非原子,sum 可能不准,只能作为近似参考 更稳妥的做法是用
LongAdder[]
,每个槽独立累加,避免遍历时锁竞争 滑动窗口真正的复杂点不在计数,而在「如何让过期槽位的计数自然归零而不影响正在累加的请求」——这需要配合时间戳标记或延迟回收,不是靠某个函数就能一键解决的。

相关文章