在并发编程中,我们经常会遇到这样的需求:
统计某个用户、接口、商品或任务被访问了多少次。
例如:
接口 A 被访问了 100 次
接口 B 被访问了 35 次
接口 C 被访问了 8 次
如果是单线程程序,直接使用 HashMap<String, Integer> 就可以。
但在多线程环境下,这种写法会有线程安全问题:
Map<String, Integer> counter = new HashMap<>();
counter.put("api:user:list",
counter.getOrDefault("api:user:list", 0) + 1);
原因是:
读取旧值
加 1
写回新值
这三个步骤并不是原子操作。
多个线程同时执行时,可能发生计数丢失。
这时就可以使用:
ConcurrentHashMap + LongAdder
一、为什么使用 ConcurrentHashMap
ConcurrentHashMap 是线程安全的 Map。
它适合多个线程同时进行:
put
get
remove
computeIfAbsent
例如:
ConcurrentHashMap<String, LongAdder> counter =
new ConcurrentHashMap<>();
这里:
Key:业务标识
Value:计数器
例如:
api:user:list -> 计数器
api:order:create -> 计数器
api:product:detail -> 计数器
ConcurrentHashMap 负责保证多个线程操作 Map 时的安全。
但它只解决了 Map 本身的并发问题,并不代表 Value 的累加操作天然安全。
二、为什么使用 LongAdder
LongAdder 是 Java 提供的并发计数器。
基本用法:
LongAdder adder = new LongAdder();
adder.increment();
adder.increment();
System.out.println(adder.sum());
输出:
2
常用方法:
adder.increment(); // 加 1
adder.decrement(); // 减 1
adder.add(10); // 加 10
adder.sum(); // 获取当前值
adder.reset(); // 重置为 0
adder.sumThenReset(); // 获取值并重置
三、ConcurrentHashMap 和 LongAdder 怎么配合
最常见的写法是:
ConcurrentHashMap<String, LongAdder> counter =
new ConcurrentHashMap<>();
counter
.computeIfAbsent("api:user:list", key -> new LongAdder())
.increment();
这段代码可以拆成两步理解。
第一步:
counter.computeIfAbsent(
"api:user:list",
key -> new LongAdder()
);
表示:
如果 Map 中已经有这个 Key,就返回原来的 LongAdder;如果没有,就创建一个新的 LongAdder 放进去。
第二步:
.increment();
表示让这个计数器加 1。
所以完整逻辑就是:
根据 Key 找到计数器
↓
不存在就创建
↓
计数器加 1
四、完整示例:接口访问次数统计
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
public class ApiAccessCounter {
private final ConcurrentHashMap<String, LongAdder> counterMap =
new ConcurrentHashMap<>();
/**
* 增加接口访问次数
*/
public void increment(String apiCode) {
counterMap
.computeIfAbsent(apiCode, key -> new LongAdder())
.increment();
}
/**
* 获取某个接口的访问次数
*/
public long getCount(String apiCode) {
LongAdder adder = counterMap.get(apiCode);
if (adder == null) {
return 0;
}
return adder.sum();
}
/**
* 获取所有接口访问次数
*/
public Map<String, Long> getAllCount() {
Map<String, Long> result = new ConcurrentHashMap<>();
counterMap.forEach((apiCode, adder) ->
result.put(apiCode, adder.sum())
);
return result;
}
/**
* 清除某个接口的计数
*/
public void remove(String apiCode) {
counterMap.remove(apiCode);
}
/**
* 清空所有计数
*/
public void clear() {
counterMap.clear();
}
}
使用:
public class Main {
public static void main(String[] args) {
ApiAccessCounter counter = new ApiAccessCounter();
counter.increment("user_list");
counter.increment("user_list");
counter.increment("order_create");
System.out.println(counter.getCount("user_list"));
System.out.println(counter.getCount("order_create"));
System.out.println(counter.getAllCount());
}
}
输出:
2
1
{user_list=2, order_create=1}
五、多线程测试
下面模拟 10 个线程,每个线程对同一个接口计数 10 万次。
理论结果应该是:
10 × 100000 = 1000000
代码:
import java.util.concurrent.CountDownLatch;
public class ConcurrentTest {
public static void main(String[] args) throws InterruptedException {
ApiAccessCounter counter = new ApiAccessCounter();
int threadCount = 10;
int loopCount = 100_000;
CountDownLatch latch =
new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
for (int j = 0; j < loopCount; j++) {
counter.increment("api:user:list");
}
} finally {
latch.countDown();
}
}).start();
}
latch.await();
System.out.println(
counter.getCount("api:user:list")
);
}
}
输出:
1000000
这说明在多个线程同时累加时,计数结果没有丢失。
六、为什么不用 Integer
有些人可能会这样写:
ConcurrentHashMap<String, Integer> counterMap =
new ConcurrentHashMap<>();
然后:
counterMap.put(
apiCode,
counterMap.getOrDefault(apiCode, 0) + 1
);
虽然 ConcurrentHashMap 本身是线程安全的,但这一整段代码并不是原子操作。
它实际上包含:
get
加 1
put
例如两个线程同时读取到:
10
线程 A 计算:
10 + 1 = 11
线程 B 也计算:
10 + 1 = 11
最后两个线程都写入 11。
正确结果本来应该是:
12
实际结果却是:
11
这就是并发计数丢失。
当然,也可以使用:
counterMap.merge(apiCode, 1, Integer::sum);
例如:
counterMap.merge(apiCode, 1, Integer::sum);
这个写法是线程安全的。
但在高并发计数场景中,LongAdder 通常更合适。
七、为什么不用 AtomicLong
也可以这样写:
ConcurrentHashMap<String, AtomicLong> counterMap =
new ConcurrentHashMap<>();
counterMap
.computeIfAbsent(
apiCode,
key -> new AtomicLong()
)
.incrementAndGet();
AtomicLong 也是线程安全的计数器。
那么它和 LongAdder 有什么区别?
AtomicLong
AtomicLong 内部维护一个值。
多个线程同时修改这个值时,会通过 CAS 不断竞争。
多个线程
↓
竞争同一个变量
竞争不激烈时,性能很好。
LongAdder
LongAdder 在竞争激烈时,会把一个计数拆成多个计数单元。
不同线程可以更新不同单元。
最后调用:
sum()
时,再把多个单元相加。
可以理解为:
线程1 -> 单元1
线程2 -> 单元2
线程3 -> 单元3
线程4 -> 单元4
最后汇总
因此,高并发写入场景下,LongAdder 通常比 AtomicLong 更高效。
八、LongAdder 的注意事项
1. sum() 不一定是绝对瞬时值
long count = adder.sum();
sum() 会汇总当前各个计数单元。
但如果其他线程正在同时执行 increment(),这个值不一定代表某个绝对精确时间点的快照。
因此,LongAdder 更适合:
统计
监控
访问次数
请求次数
日志数量
指标累加
不太适合要求强一致性的业务。
例如:
账户余额
库存扣减
额度扣减
支付金额
这些场景不能只依赖 LongAdder。
2. LongAdder 不适合条件判断后扣减
例如下面这种业务:
剩余额度大于 0 才允许访问
访问后额度减 1
如果写成:
if (adder.sum() < limit) {
adder.increment();
}
这不是线程安全的整体操作。
因为:
判断
累加
是两个步骤。
多个线程可能同时判断成功,然后一起累加,最终超过限制。
所以 LongAdder 更适合做统计,不适合做严格限流和额度控制。
严格限流通常需要:
AtomicLong
synchronized
Lock
Redis Lua
数据库乐观锁
Semaphore
3. Map 中的 Key 可能越来越多
例如按照用户 ID 计数:
counter.increment("user:" + userId);
如果用户数量持续增长,ConcurrentHashMap 中的数据也会越来越多。
需要考虑:
定时清理
设置过期时间
定期持久化
按时间窗口重建 Map
ConcurrentHashMap 本身不支持过期时间。
如果需要自动过期,可以考虑:
Caffeine
Redis
Guava Cache
九、Spring Boot 中使用
在 Spring Boot 中,可以把计数器注册为单例 Bean。
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
@Component
public class ApiCounter {
private final ConcurrentHashMap<String, LongAdder> counterMap =
new ConcurrentHashMap<>();
public void increment(String apiCode) {
counterMap
.computeIfAbsent(apiCode, key -> new LongAdder())
.increment();
}
public long getCount(String apiCode) {
LongAdder adder = counterMap.get(apiCode);
return adder == null ? 0 : adder.sum();
}
}
Controller 中使用:
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class UserController {
private final ApiCounter apiCounter;
@GetMapping("/user/list")
public String list() {
apiCounter.increment("user_list");
return "success";
}
@GetMapping("/user/list/count")
public long count() {
return apiCounter.getCount("user_list");
}
}
因为 ApiCounter 是 Spring 单例 Bean,所以所有请求共享同一个:
ConcurrentHashMap<String, LongAdder>
不会每次请求都创建一个新的计数器。
十、按用户和接口统计
如果需要统计:
某个用户访问某个接口多少次
可以把用户 ID 和接口编码组合成 Key。
public void increment(Long userId, String apiCode) {
String key = userId + ":" + apiCode;
counterMap
.computeIfAbsent(
key,
value -> new LongAdder()
)
.increment();
}
例如:
1001:user_list
1001:order_create
1002:user_list
完整示例:
@Component
public class UserApiCounter {
private final ConcurrentHashMap<String, LongAdder> counterMap =
new ConcurrentHashMap<>();
public void increment(Long userId, String apiCode) {
String key = buildKey(userId, apiCode);
counterMap
.computeIfAbsent(
key,
value -> new LongAdder()
)
.increment();
}
public long getCount(Long userId, String apiCode) {
String key = buildKey(userId, apiCode);
LongAdder adder = counterMap.get(key);
return adder == null ? 0 : adder.sum();
}
private String buildKey(Long userId, String apiCode) {
return userId + ":" + apiCode;
}
}
十一、按天统计接口访问量
如果要按天统计,可以把日期加入 Key。
import java.time.LocalDate;
public void increment(String apiCode) {
String key = apiCode + ":" + LocalDate.now();
counterMap
.computeIfAbsent(
key,
value -> new LongAdder()
)
.increment();
}
生成的 Key 类似:
user_list:2026-08-04
order_create:2026-08-04
不过这种方式要注意定期清理历史日期数据。
十二、一个更完整的统计服务
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
@Service
public class ApiStatisticsService {
private final ConcurrentHashMap<String, LongAdder> counterMap =
new ConcurrentHashMap<>();
public void increment(String apiCode) {
String key = buildKey(apiCode, LocalDate.now());
counterMap
.computeIfAbsent(
key,
value -> new LongAdder()
)
.increment();
}
public long getTodayCount(String apiCode) {
String key = buildKey(apiCode, LocalDate.now());
LongAdder adder = counterMap.get(key);
return adder == null ? 0 : adder.sum();
}
public Map<String, Long> getAllStatistics() {
Map<String, Long> result = new HashMap<>();
counterMap.forEach((key, adder) ->
result.put(key, adder.sum())
);
return result;
}
public long getAndReset(String apiCode) {
String key = buildKey(apiCode, LocalDate.now());
LongAdder adder = counterMap.get(key);
if (adder == null) {
return 0;
}
return adder.sumThenReset();
}
public void remove(String apiCode, LocalDate date) {
counterMap.remove(buildKey(apiCode, date));
}
private String buildKey(
String apiCode,
LocalDate date
) {
return apiCode + ":" + date;
}
}
十三、适用场景
ConcurrentHashMap + LongAdder 比较适合:
接口访问次数统计
商品浏览次数统计
异常出现次数统计
任务执行次数统计
不同状态数量统计
日志级别数量统计
在线请求指标统计
本地缓存命中次数统计
例如:
counterMap
.computeIfAbsent(
"cache_hit",
key -> new LongAdder()
)
.increment();
或者:
counterMap
.computeIfAbsent(
exception.getClass().getSimpleName(),
key -> new LongAdder()
)
.increment();
十四、不适用场景
它不适合:
分布式计数
严格库存扣减
账户余额计算
支付金额计算
强一致额度控制
跨服务器共享状态
因为这个 Map 只存在当前 JVM 内存中。
如果应用部署了三台服务器:
服务器 A:100 次
服务器 B:80 次
服务器 C:60 次
每台服务器都有自己的计数器。
它们不会自动合并。
分布式环境中,可以考虑:
Redis INCR
Redis Hash
消息队列
数据库原子更新
Prometheus Metrics
十五、总结
ConcurrentHashMap 和 LongAdder 的职责不同。
ConcurrentHashMap 负责:
保证多个线程安全地访问 Map
LongAdder 负责:
保证多个线程高效地进行计数
最常见的组合写法是:
counterMap
.computeIfAbsent(
key,
value -> new LongAdder()
)
.increment();
可以记成一句话:
ConcurrentHashMap管理多个计数器,LongAdder负责每个计数器的并发累加。
在单机高并发统计场景中,这是一种简单、实用而且性能很好的实现方式。
评论
填写昵称与邮箱即可评论,无需登录。