concurrenthashmap是java并发包中线程安全的哈希表实现,采用分段锁(jdk 1.7)或cas+synchronized(jdk 1.8+)机制提升并发性能,支持高并发读写且提供原子操作如computeifabsent。

Java中用Semaphore做限流,本身是全局计数;若想按业务维度(比如用户ID、接口路径)分别控制并发量,就得结合ConcurrentHashMap实现“分段限流”。这不是简单把两者拼在一起,而是用ConcurrentHashMap管理多个独立的Semaphore实例,每个键对应一个限流单元。
分段限流的核心设计逻辑
关键在于避免全局锁竞争,同时保证每个分段的Semaphore创建和访问线程安全。ConcurrentHashMap天然支持高并发读写,但要注意:不能直接put一个新Semaphore后立刻acquire——可能多个线程同时发现key不存在、都去new Semaphore,造成重复初始化。
- 用computeIfAbsent方法一次性完成“查+建+存”,内部已加锁保障原子性
- 每个分段的Semaphore应设为公平模式(new Semaphore(permits, true)),防止某个key长期饥饿
- 需考虑内存泄漏:长期运行中,冷门key对应的Semaphore不会自动清理,建议配合定时任务或弱引用缓存策略
典型代码结构示例
以下是一个按userId分段限流的简化实现:
private final ConcurrentHashMap<string semaphore> userSemaphores = new ConcurrentHashMap();
public boolean tryAcquireForUser(String userId, int permits) {
// 每个用户最多允许5个并发
Semaphore sem = userSemaphores.computeIfAbsent(userId,
id -> new Semaphore(5, true));
return sem.tryAcquire(permits, 100, TimeUnit.MILLISECONDS);
}
public void releaseForUser(String userId, int permits) {
Semaphore sem = userSemaphores.get(userId);
if (sem != null) {
sem.release(permits);
}
}</string>
注意:release前必须判空,因为computeIfAbsent不保证该key后续一直存在;若业务上允许动态回收,可在release后检查availablePermits()是否回到初始值,再remove key。
性能与边界注意事项
分段数量大幅增加时,内存占用和GC压力会上升,尤其当单机承载几十万不同userId时。此时需权衡:
- 限制分段总数(如LRU缓存最多1000个活跃用户Semaphore)
- 对超时未使用的Semaphore调用reducePermits逐步归零,再触发remove
- 避免在高频路径(如每毫秒调用)中反复构造key字符串,可复用StringBuilder或使用Long类型ID减少对象创建
和ConcurrentHashMap自身线程安全无关的误区
有人误以为“用了ConcurrentHashMap就不用管Semaphore线程安全了”——其实不然。ConcurrentHashMap只保征map操作安全,每个value(即Semaphore)仍需独立调用acquire/release。Semaphore本身是线程安全的,但它的状态变化(比如许可数)必须由业务逻辑正确配对,否则会出现许可泄露或死锁。
例如:tryAcquire成功后,若业务异常没走到finally块里的release,该分段就会永久少一个许可。务必用try-finally包裹核心逻辑,或用try-with-resources封装(需自定义AutoCloseable包装类)。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











