跳到正文
hello world

ConcurrentHashMap 和 LongAdder 的配合使用

在并发编程中,我们经常会遇到这样的需求:

统计某个用户、接口、商品或任务被访问了多少次。

例如:

接口 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

十五、总结

ConcurrentHashMapLongAdder 的职责不同。

ConcurrentHashMap 负责:

保证多个线程安全地访问 Map

LongAdder 负责:

保证多个线程高效地进行计数

最常见的组合写法是:

counterMap
        .computeIfAbsent(
                key,
                value -> new LongAdder()
        )
        .increment();

可以记成一句话:

ConcurrentHashMap 管理多个计数器,LongAdder 负责每个计数器的并发累加。

在单机高并发统计场景中,这是一种简单、实用而且性能很好的实现方式。

评论

填写昵称与邮箱即可评论,无需登录。

推荐阅读