在高并发编程中,ConcurrentHashMap 是我们处理线程安全映射的常用工具,但如果使用不当,不仅无法发挥其并发优势,反而会让代码性能跌入谷底。本文将通过一个真实的反面案例,拆解并发计数代码的核心问题,一步步优化并验证性能提升效果。
一、场景背景:多线程统计随机Key出现次数
我们的需求很简单:
- 启动10个线程,循环1000万次
- 随机生成10个Key(item0~item9)
- 统计每个Key被命中的次数
- 保证线程安全,最终计数准确
先看原始实现代码(反面教材):
java
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
public class OriginalCounterWithTime {
// 循环次数
private static int LOOP_COUNT = 10000000;
// 线程数量
private static int THREAD_COUNT = 10;
// 元素数量
private static int ITEM_COUNT = 10;
private Map<String, Long> normaluse() throws InterruptedException {
// 记录方法开始时间(毫秒)
long startTime = System.currentTimeMillis();
ConcurrentHashMap<String, Long> freqs = new ConcurrentHashMap<>(ITEM_COUNT);
ForkJoinPool forkJoinPool = new ForkJoinPool(THREAD_COUNT);
forkJoinPool.execute(() -> IntStream.rangeClosed(1, LOOP_COUNT).parallel().forEach(i -> {
// 获得一个随机的Key
String key = "item" + ThreadLocalRandom.current().nextInt(ITEM_COUNT);
synchronized (freqs) {
if (freqs.containsKey(key)) {
// Key存在则+1
freqs.put(key, freqs.get(key) + 1);
} else {
// Key不存在则初始化为1
freqs.put(key, 1L);
}
}
}));
forkJoinPool.shutdown();
forkJoinPool.awaitTermination(1, TimeUnit.HOURS);
// 计算总耗时并打印
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;
System.out.println("=== 原始代码执行耗时统计 ===");
System.out.println("循环次数:" + LOOP_COUNT);
System.out.println("线程数量:" + THREAD_COUNT);
System.out.println("总耗时:" + costTime + " 毫秒(" + (costTime / 1000.0) + " 秒)");
return freqs;
}
// 主方法:程序入口,可直接运行
public static void main(String[] args) throws InterruptedException {
OriginalCounterWithTime counter = new OriginalCounterWithTime();
Map<String, Long> result = counter.normaluse();
// 打印统计结果并校验总数(验证计数是否准确)
System.out.println("\n=== 统计结果 ===");
long total = 0;
for (Map.Entry<String, Long> entry : result.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
total += entry.getValue();
}
System.out.println("统计总数:" + total);
System.out.println("预期总数:" + LOOP_COUNT);
System.out.println("计数是否准确:" + (total == LOOP_COUNT));
}
}
二、原始代码的核心问题剖析
1. 最大问题:完全浪费 ConcurrentHashMap 的并发优势
ConcurrentHashMap 的设计核心是细粒度锁 (JDK1.8后为CAS+分段synchronized),原本支持不同Key的操作并行执行。但代码中 synchronized (freqs) 对整个Map加了全局锁,导致所有线程无论操作哪个Key都必须排队,相当于把并发程序变成了串行执行。
形象点说:就像买了一辆跑车(ConcurrentHashMap),却用绳子绑死车轮(全局锁),跑车只能像自行车一样慢走。
2. 操作冗余且不够优雅
即使加了全局锁,containsKey() + get() + put() 是三次独立的哈希查找操作,虽然锁能保证安全,但多了一次无意义的 containsKey() 检查,增加了性能开销。
3. 资源管理不严谨
forkJoinPool.shutdown() 未放在 finally 块中,若执行过程中抛出异常,线程池无法关闭,会导致系统资源泄漏(线程、CPU占用)。
4. 鲁棒性差
核心逻辑无异常处理,一旦并行流内部出错,程序直接中断且无法定位问题。
三、原始代码性能测试
运行上述代码,在普通8核CPU机器上的结果:
=== 原始代码执行耗时统计 ===
循环次数:10000000
线程数量:10
总耗时:1280 毫秒(1.28 秒)
=== 统计结果 ===
item0: 999876
item1: 1000123
item2: 999988
...
统计总数:10000000
预期总数:10000000
计数是否准确:true
关键结论:计数结果准确,但10个线程的执行耗时和单线程几乎无差异(甚至略慢,因为加解锁有额外开销)。
四、代码优化方案与实现
针对上述问题,我们做核心优化:
优化核心思路
- 移除全局
synchronized锁,使用ConcurrentHashMap内置的原子方法 - 简化计数逻辑,减少冗余操作
- 完善资源管理和异常处理
优化后的完整代码:
java
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
/**
* 优化后的并发计数代码 - 保留耗时统计,方便对比
*/
public class OptimizedCounterWithTime {
// 循环次数
private static int LOOP_COUNT = 10000000;
// 线程数量
private static int THREAD_COUNT = 10;
// 元素数量
private static int ITEM_COUNT = 10;
private Map<String, Long> optimizedUse() throws InterruptedException {
// 记录方法开始时间(毫秒)
long startTime = System.currentTimeMillis();
ConcurrentHashMap<String, Long> freqs = new ConcurrentHashMap<>(ITEM_COUNT);
ForkJoinPool forkJoinPool = new ForkJoinPool(THREAD_COUNT);
try {
forkJoinPool.execute(() -> IntStream.rangeClosed(1, LOOP_COUNT)
.parallel()
.forEach(i -> {
// 获得一个随机的Key
String key = "item" + ThreadLocalRandom.current().nextInt(ITEM_COUNT);
// 核心优化:使用ConcurrentHashMap原子方法替代全局synchronized锁
// compute方法本身是线程安全的,且锁粒度仅针对单个key
freqs.compute(key, (k, v) -> v == null ? 1L : v + 1);
}));
} finally {
// 确保线程池一定会关闭,避免资源泄漏
forkJoinPool.shutdown();
forkJoinPool.awaitTermination(1, TimeUnit.HOURS);
}
// 计算总耗时并打印
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;
System.out.println("=== 优化后代码执行耗时统计 ===");
System.out.println("循环次数:" + LOOP_COUNT);
System.out.println("线程数量:" + THREAD_COUNT);
System.out.println("总耗时:" + costTime + " 毫秒(" + (costTime / 1000.0) + " 秒)");
return freqs;
}
// 主方法:程序入口,可直接运行
public static void main(String[] args) throws InterruptedException {
OptimizedCounterWithTime counter = new OptimizedCounterWithTime();
Map<String, Long> result = counter.optimizedUse();
// 打印统计结果并校验总数(验证计数准确性)
System.out.println("\n=== 统计结果 ===");
long total = 0;
for (Map.Entry<String, Long> entry : result.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
total += entry.getValue();
}
System.out.println("统计总数:" + total);
System.out.println("预期总数:" + LOOP_COUNT);
System.out.println("计数是否准确:" + (total == LOOP_COUNT));
}
}
核心优化点说明
| 优化维度 | 原始问题 | 优化方案 | 优化效果 |
|---|---|---|---|
| 锁机制 | 全局synchronized锁(并发变串行) | 移除全局锁,使用compute()原子方法 |
锁粒度缩小到单个Key,真正并发执行 |
| 操作效率 | 三次哈希查找(containsKey+get+put) | compute()一次原子操作完成计数逻辑 |
减少哈希开销,逻辑更简洁 |
| 资源管理 | 线程池可能泄漏 | try-finally包裹,确保线程池必关闭 | 避免系统资源泄漏 |
| 代码健壮性 | 无异常处理 | 核心逻辑包裹在try块中 | 鲁棒性提升 |
五、优化后性能测试
同样在8核CPU机器上运行优化后的代码,结果如下:
=== 优化后代码执行耗时统计 ===
循环次数:10000000
线程数量:10
总耗时:260 毫秒(0.26 秒)
=== 统计结果 ===
item0: 999789
item1: 1000211
item2: 999956
...
统计总数:10000000
预期总数:10000000
计数是否准确:true
性能对比:
- 原始代码:约1280ms(1.28秒)
- 优化后代码:约260ms(0.26秒)
- 性能提升:近5倍(不同机器略有差异)
六、进阶优化:超高并发场景(可选)
如果循环次数达到上亿次 ,可以进一步用 LongAdder 替代 Long 类型,LongAdder 专为高并发计数设计,性能比直接自增 Long 更高:
java
// 替换Map定义
private Map<String, LongAdder> freqs = new ConcurrentHashMap<>(ITEM_COUNT);
// 计数逻辑改为
freqs.computeIfAbsent(key, k -> new LongAdder()).increment();
// 最终取值时转换为Long
Map<String, Long> result = freqs.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().longValue()));
七、总结
- 核心避坑点 :使用
ConcurrentHashMap时,避免对整个Map加全局锁,否则会完全丧失其并发优势; - 最佳实践 :优先使用
ConcurrentHashMap内置的原子方法(compute()/merge()/computeIfAbsent())实现线程安全操作,锁粒度更细、性能更好; - 资源管理 :线程池等资源必须在
finally块中确保关闭,避免资源泄漏; - 性能优化 :超高并发计数场景,优先选择
LongAdder替代Long自增。
通过本次优化,我们不仅解决了代码的性能问题,也掌握了高并发下 ConcurrentHashMap 的正确使用方式,避免从"并发优化"变成"并发负优化"。