HashMap并发死循环
HashMap内存泄漏
ConcurrentHashMap误用
ArrayList并发修改
ArrayList大数据OOM
LinkedList误用
TreeMap排序异常
unmodifiableList误用
🔴 场景1: HashMap 并发死循环 — CPU 100%
🔴 HashMap 并发死循环 — 多线程put导致CPU飙到100%
🎬 什么时候出:
多个线程同时往同一个 HashMap 里 put 数据,恰好触发了扩容(resize)。在 Java 7 及之前的 HashMap 中,并发扩容可能导致链表形成环形结构,造成死循环。
🚨
线上告警: 订单统计服务 CPU 持续 100%,所有接口超时!
📋 问题代码
OrderStatsService.java
OrderController.java
📁 service
☕ OrderStatsService
📁 controller
☕ OrderController
📁 model
☕ Order
1 public class OrderStatsService {
2
3 // ❌ 线程不安全!多个线程同时写
4 private Map <String , Integer > statsMap = new HashMap <>();
5
6 // 多个线程同时调用这个方法
7 public void recordOrder (String product) {
8 Integer count = statsMap.get (product);
9 statsMap.put (product, count == null ? 1 : count + 1 );
10 }
11 }
等一下... HashMap 不是线程安全的!如果有 10 个线程同时 put,而且恰好 HashMap 需要扩容(从 16 扩到 32),两个线程同时做 resize,链表节点可能反转形成环...
📊 怎么发现
14:30
运维
监控机器人
⚠️ order-service CPU使用率 99.8%,持续10分钟,接口全部超时!
🔍 排查过程
14:32 第一步:找到高CPU的线程
$ top -Hp 12345
PID USER %CPU COMMAND
6789 app 98.2 java
6790 app 95.7 java
6791 app 2.1 java
$ printf "%x\n" 6789
1a85
$ jstack 12345 | grep -A 30 "0x1a85"
"http-nio-8080-exec-3" #58 daemon prio=5 os_prio=0 tid=0x00007f8c40123000 nid=0x1a85 runnable [0x00007f8c15e5e000]
java.lang.Thread.State: RUNNABLE
at java.util.HashMap.putVal(HashMap.java:662)
at java.util.HashMap.put(HashMap.java:611)
at com.milktea.service.OrderStatsService.recordOrder(OrderStatsService.java:9)
at com.milktea.controller.OrderController.createOrder(OrderController.java:23)
14:35 发现多个线程都卡在 HashMap.putVal()!
$ jstack 12345 | grep -c "HashMap.putVal"
12
🧠 原因分析 — 链表怎么成环的?
📍 HashMap 扩容时链表反转导致环形结构
正常状态(HashMap table[3] 的链表):
并发扩容时(两个线程同时 transfer):
线程A扩容到一半被挂起,线程B完成了扩容(链表反转为 音箱→鼠标→键盘→null)
线程A醒来继续用旧引用工作,结果 B→A(已反转)和 A→B(旧引用)同时存在
形成环形链表:
⚠️ 注意: Java 8 改进了 resize 逻辑(用尾插法代替头插法),不会形成环形链表了。但 HashMap 仍然不是线程安全的!并发 put 仍然会导致数据丢失、size 不对、NullPointerException 等问题。
✅ 修复
❌ 修复前 — HashMap(不安全)
1 private Map <String ,Integer > statsMap = new HashMap <>();
✅ 修复后 — ConcurrentHashMap(安全)
1 private Map <String ,Integer > statsMap = new ConcurrentHashMap <>();
但等一下... 即使换了 ConcurrentHashMap,recordOrder 里的 get + put 也是复合操作,不是原子的!还是用 compute 方法吧:
OrderStatsService.java (修复版)
📁 service
☕ OrderStatsService
1 public class OrderStatsService {
2 private Map <String , Integer > statsMap = new ConcurrentHashMap <>();
3
4 // ✅ 用 compute 保证原子性
5 public void recordOrder (String product) {
6 statsMap.compute (product, (k, v) -> v == null ? 1 : v + 1 );
7 }
8 }
🧪 验证修复
$ wrk -t12 -c1000 -d30s http://localhost:8080/order/create
Thread Stats Avg Stdev Max
Latency 12ms 5ms 89ms ✅ 不再超时
$ jstack 12345 | grep -c "HashMap"
0 ✅ 没有线程卡在HashMap了
$ top -Hp 12345
PID USER %CPU COMMAND
6789 app 12.3 java ✅ CPU正常
✅ 结果: CPU从99.8%降到12.3%,接口响应正常,statsMap数据正确无丢失。
🟡 场景2: HashMap 内存泄漏 — Key对象被修改了
🟡 HashMap 内存泄漏 — 用可变对象做Key,改了以后再也拿不出来
🎬 什么时候出:
你用一个自定义对象(比如Store)做HashMap的Key,但put之后有人改了这个对象的字段。因为hashCode()基于字段计算,改了字段后hashCode变了,这个entry就"丢了"——get不到、remove不掉,但还占着内存。
📋 问题代码
Store.java
StoreService.java
📁 model
☕ Store
📁 service
☕ StoreService
1 public class Store {
2 private String name; // 店名 — 会变!
3 private String address;
4
5 // hashCode 基于 name 计算
6 @Override
7 public int hashCode () { return name.hashCode (); }
8 @Override
9 public boolean equals (Object o) { ... name.equals (...) ... }
10 }
1 public class StoreService {
2 private Map <Store , List <Order >> storeOrderCache = new HashMap <>();
3
4 public void cacheOrders (Store store, List <Order > orders) {
5 storeOrderCache.put (store, orders); // put时 hashCode=store.name的hash
6 }
7 }
8
9 // ❌ 某天运营改了店名
10 store.setName ("朝阳旗舰店" ); // hashCode变了!
11 storeOrderCache.get (store); // → null!找不到
12 storeOrderCache.size (); // → 1(还在,但永远拿不出来)
📊 原因图解
📍 put 时 vs get 时的 hashCode 不一样了
Put 时:name="朝阳店",hashCode=12345 → 存在 table[7 ]
table[7]
Store(朝阳店)→[orders]
改名字后 get 时:name="朝阳旗舰店",hashCode=67890 → 去 table[3 ] 找
而真正的数据还在 →
table[7]
Store(朝阳旗舰店)→[orders] ← 孤儿!
🔍 怎么发现
$ jmap -histo 12345 | head -20
num #instances #bytes class name
1: 489213 624587200 java.util.HashMap$Node
2: 28456 45892312 com.milktea.model.Store
3: 15623 12567890 java.util.ArrayList
✅ 修复方案
❌ 用可变对象做Key
1 // Store的name会变,hashCode不稳定
2 Map <Store , List <Order >> cache = new HashMap <>();
✅ 用不可变的ID做Key
1 // 用storeId(String不可变)做Key
2 Map <String , List <Order >> cache = new HashMap <>();
3 cache.put (store.getId (), orders);
✅ 验证: 改完后重启,jmap观察一周,HashMap$Node数量稳定在200+,不再增长。内存恢复正常。
🏗️ HashMap vs ConcurrentHashMap 结构对比
HashMap — 无同步保护
单个数组 + 链表/红黑树,多线程竞争同一张表
❌ 无同步 → 多线程不安全,并发扩容可死循环
ConcurrentHashMap — 分段锁 / CAS
JDK7: Segment分段锁 | JDK8: Node[] + CAS + synchronized
✅ 分段锁/CAS → 线程安全,高并发读写
⚠️ 但注意:ConcurrentHashMap 保证单次操作的线程安全,复合操作仍需额外同步!
if(map.get(key)==null) map.put(key,val) 这种"先查再改"不是原子的 → 详见下方场景3
🟣 场景3: ConcurrentHashMap 误用 — 库存扣成负数
🟣 ConcurrentHashMap 误用 — 复合操作不是原子的!库存扣成负数
🎬 什么时候出:
你以为用了 ConcurrentHashMap 就线程安全了,但你的代码是"先get再判断再put"三步走。虽然每一步都是线程安全的,但三步之间其他线程可以插进来,导致最终结果不对。
🚨
线上问题: 商品"无线耳机"库存出现 -3 !明明库存只有5杯,却卖了8杯!
📋 问题代码
📁 service
☕ InventoryService
1 public class InventoryService {
2 private ConcurrentHashMap <String , Integer > stock = new ConcurrentHashMap <>();
3
4 // ❌ 以为线程安全,其实不是!
5 public boolean deductStock (String product, int qty) {
6 Integer current = stock.get (product); // 步骤1:读(原子)
7 if (current != null && current >= qty) { // 步骤2:判断
8 stock.put (product, current - qty); // 步骤3:写(原子)
9 return true ;
10 }
11 return false ;
12 }
13 }
📊 竞态条件图解
📍 两个用户同时买"无线耳机",库存剩5杯,每人买3杯
T1
线程A
stock.get("无线耳机") → 5
T2
线程B
stock.get("无线耳机") → 5
T6
线程B
stock.put → 5-3 = 2 (应该是-1但覆盖了)
T7
结果
stock = 2,但实际卖了6杯(5杯库存卖了6杯 = 超卖1杯 )
如果3个用户同时买呢?库存5杯卖9杯 = 库存变 -1
✅ 修复方案
InventoryService.java (修复版)
📁 service
☕ InventoryService
1 public class InventoryService {
2 // 方案1:用 compute 保证原子性(推荐)
3 public boolean deductStock (String product, int qty) {
4 AtomicBoolean success = new AtomicBoolean (false );
5 stock.compute (product, (k, v) -> {
6 if (v != null && v >= qty) {
7 success.set (true );
8 return v - qty;
9 }
10 return v;
11 });
12 return success.get ();
13 }
14
15 // 方案2:用 AtomicLong 更简洁
16 private ConcurrentHashMap <String , AtomicLong > stock2 = new ConcurrentHashMap <>();
17
18 public boolean deductStock2 (String product, int qty) {
19 AtomicLong count = stock2.get (product);
20 while (true ) {
21 long cur = count.get ();
22 if (cur < qty) return false ;
23 if (count.compareAndSet (cur, cur - qty)) return true ;
24 }
25 }
26 }
✅ 验证: 压测100并发同时扣库存,库存始终不为负数,所有成功扣减数量之和 = 初始库存。
📦 ArrayList vs LinkedList 内存布局对比
ArrayList — 连续内存数组
[0]
[1]
[2]
[3]
[4]
[5]
[6]
[7]
[8]
[9]
内存地址连续,CPU缓存友好
随机访问 O(1)
尾部插入 O(1)*
中间插入 O(n)
✅ 连续内存 → O(1) 随机访问,适合读多写少
LinkedList — 分散链式节点
prev |val |next
→
prev |val |next
→
prev |val |next
→
prev |val |next
节点分散在堆内存各处,无缓存局部性
随机访问 O(n)
头尾插入 O(1)
中间插入 O(1)*
⚠️ 分散节点 → 只能从头遍历 O(n),频繁get()极慢
💡 选型建议
绝大多数场景用 ArrayList(读多写少、随机访问)。只有频繁在头/尾插入删除时才考虑 LinkedList。
🟢 场景4: ArrayList ConcurrentModificationException
🟢 ArrayList 并发修改 — 遍历时另一个线程 add,直接报错
🎬 什么时候出:
一个线程用 for-each 遍历 ArrayList,另一个线程同时 add 或 remove。ArrayList 内部有个 modCount 计数器,遍历时检查发现 modCount 变了,直接抛 ConcurrentModificationException。
📋 问题代码
📁 service
☕ OrderProcessor
1 public class OrderProcessor {
2 // ❌ 线程不安全的ArrayList
3 private List <Order > pendingOrders = new ArrayList <>();
4
5 // 线程A:定时处理待处理订单(每5秒)
6 @Scheduled (fixedRate = 5000 )
7 public void processOrders () {
8 for (Order order : pendingOrders) { // ❌ for-each遍历
9 process (order);
10 }
11 }
12
13 // 线程B:用户下单添加新订单(HTTP请求线程)
14 public void addOrder (Order order) {
15 pendingOrders.add (order); // ❌ 同时修改!modCount++
16 }
17 }
📊 异常日志
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.milktea.service.OrderProcessor.processOrders(OrderProcessor.java:8)
✅ 修复
✅ 方案1:CopyOnWriteArrayList
1 // 读多写少时推荐
2 private List <Order > pending = new CopyOnWriteArrayList <>();
3 // add时复制整个数组,遍历用旧数组
4 // 不会ConcurrentModificationException!
✅ 方案2:加锁
1 synchronized (pendingOrders) {
2 for (Order o : pendingOrders) {
3 process (o);
4 }
5 }
💡 单线程也可能出这个错: 在 for-each 里删除元素也会 CME!正确做法是用迭代器的 remove() 方法,或者用 removeIf()。
📈 HashMap 扩容过程图
容量16负载因子 0.75
→
元素 > 12触发扩容阈值
→
创建新数组新容量 = 32
→
重新hash分配所有元素重新计算位置
→
扩容完成继续正常操作
扩容前 — 容量16
[0] k1
[1] k2→k7
[2]
[3] k3→k5→k9
[4]
[5] k6
…
→
扩容后 — 容量32
[0] k1
[1] k2
[2]
[3] k3
[19] k5
[17] k7→k9
…
⚠️ 扩容要重新计算所有元素位置(rehash),很耗时!如果在并发环境下,多个线程同时扩容可能导致链表成环 → 死循环(场景1)
🔴 场景5: ArrayList 大数据 OOM — 一次性加载500万条
🔴 ArrayList 大数据 OOM — 一次性查500万订单,直接OOM
🎬 什么时候出:
导出功能直接 selectList(null) 把全表数据加载到内存。表里500万行,每个Order对象约500字节,500万 × 500字节 = 2.5GB 。而JVM堆才2GB,直接OOM。
📋 问题代码
OrderExportService.java
OrderMapper.xml
📁 service
☕ OrderExportService
📁 mapper
📄 OrderMapper.xml
1 public class OrderExportService {
2
3 // ❌ 直接查全部!
4 public void exportAll () {
5 List <Order > all = orderMapper.selectList (null );
6 // 数据库500万订单 → ArrayList要2.5GB内存
7 // JVM -Xmx2g → 💥 OOM!
8 for (Order o : all) {
9 writeToExcel (o);
10 }
11 }
12 }
📊 OOM日志
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.ArrayList.grow(ArrayList.java:248)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:181)
✅ 修复:三种方案
1 // ✅ 方案1:分批查询(最常用)
2 int pageSize = 1000 ;
3 int lastId = 0 ;
4 while (true ) {
5 List <Order > batch = orderMapper.selectBatch (lastId, pageSize);
6 if (batch.isEmpty ()) break ;
7 writeToExcel (batch);
8 lastId = batch.get (batch.size()-1 ).getId ();
9 batch = null ; // 帮助GC
10 }
1 // ✅ 方案2:MyBatis Cursor(推荐)
2 try (Cursor <Order > cursor = orderMapper.selectCursor ()) {
3 cursor.forEach (order -> writeToExcel (order));
4 // Cursor每次只加载一条到内存!
5 }
1 // ✅ 方案3:EasyExcel(导出大文件最佳)
2 String file = "/tmp/orders.xlsx" ;
3 EasyExcel .write (file, Order .class ).sheet ("订单" )
4 .registerWriteHandler (new ChunkWriteHandler ())
5 .doWrite (() -> new OrderDataProvider (orderMapper));
🔵 场景6: LinkedList 误用 — 随机访问 O(n) 很慢
🔵 LinkedList 误用 — 当队列用却频繁 get(position)
🎬 什么时候出:
用 LinkedList 当队列,这没问题(addLast/removeFirst 都是 O(1))。但有人在代码里调了 get(index) 或 indexOf(),LinkedList 的随机访问是 O(n),从头遍历,数据多了就巨慢。
📋 问题代码
1 public class QueueService {
2 private LinkedList <Order > queue = new LinkedList <>();
3
4 public void enqueue (Order o) { queue.addLast (o); } // O(1) ✓
5 public Order dequeue () { return queue.removeFirst (); } // O(1) ✓
6
7 // ❌ 新需求:查看第N个人的订单
8 public Order peekAt (int pos) {
9 return queue.get (pos); // O(n)!从头遍历到pos
10 }
11
12 // ❌ 查找某个订单在队伍中的位置
13 public int findPosition (Order o) {
14 return queue.indexOf (o); // O(n) 遍历整个链表
15 }
📊 Arthas 性能分析
$ trace com.milktea.service.QueueService peekAt
+---[452.3ms ] java.util.LinkedList:get()
+---[449.8ms ] java.util.LinkedList:node()
# node()从头遍历5000个节点!
$ trace com.milktea.service.QueueService peekAtArrayList
+---[0.02ms ] java.util.ArrayList:get()
# 直接通过下标访问数组,O(1)
✅ 修复
选型原则:
• 纯队列操作(addLast/removeFirst)→ LinkedList 或 ArrayDeque 都行
• 需要随机访问 get(index) → 用 ArrayList
• 需要队列+随机访问 → 用 ArrayList + 头指针
• Java 推荐用 ArrayDeque 代替 LinkedList 当队列(性能更好,缓存更友好)
🟠 场景7: TreeMap 排序异常 — 同分产品被去重了
🟠 TreeMap / TreeSet 排序异常 — compareTo 不一致导致数据丢失
🎬 什么时候出:
往 TreeSet 里放对象,对象的 compareTo 只比较了一个字段(比如评分rating)。两个不同产品评分相同,compareTo 返回 0,TreeSet 认为"相等"只保留一个。结果你的产品列表少了好多数据!
📋 问题代码
Product.java
MenuService.java
1 public class Product implements Comparable <Product > {
2 private String name;
3 private double rating;
4
5 @Override
6 public int compareTo (Product other) {
7 return Double .compare (this .rating, other.rating);
8 // ❌ 只比较了rating!
9 // 如果两个产品rating相同,compareTo返回0
10 // TreeSet认为它们"相等",只保留一个
11 }
12 }
$ TreeSet<Product> sorted = new TreeSet<>();
sorted.add(new Product("键盘", 4.5));
sorted.add(new Product("蓝牙鼠标", 4.5)); // rating也是4.5
sorted.add(new Product("机械键盘", 4.5)); // rating也是4.5
sorted.size(); → 1 // 应该是3!只剩了"键盘"
✅ 修复
1 @Override
2 public int compareTo (Product other) {
3 int cmp = Double .compare (this .rating, other.rating);
4 if (cmp != 0 ) return cmp;
5 return this .name.compareTo (other.name); // ✅ 加上第二排序条件
6 }
🟢 场景8: unmodifiableList 误用 — 返回了"不可变"但数据还是变了
🟢 Collections.unmodifiableList 误用 — 包装了但原List变了,"不可变"也跟着变
🎬 什么时候出:
你用 Collections.unmodifiableList() 包装后返回,以为外部改不了。但如果有人修改了原始的 ArrayList,"不可变"的包装视图也跟着变了!因为它们共享同一个底层数组。
📋 问题代码
1 public class MenuService {
2 private List <Product > products = new ArrayList <>();
3
4 // 返回"不可修改"的菜单
5 public List <Product > getMenu () {
6 return Collections .unmodifiableList (products);
7 // ❌ 只是包装了一层,底层数组还是共享的!
8 }
9
10 public void addProduct (Product p) {
11 products.add (p); // 修改原list
12 // 之前返回的"不可变列表"也跟着变了!
13 }
14 }
📊 验证这个问题
$ // 测试代码
MenuService service = new MenuService ();
List <Product > menu = service.getMenu (); // 拿到"不可变"菜单
menu.size(); → 5
service.addProduct (new Product ("新品技术" ));
menu.size(); → 6!变了!
// menu是"不可变"的,但底层products变了,menu也跟着变了
// 外部调用方看到的菜单突然多了个产品,但没人调用过修改接口!
✅ 修复
1 public List <Product > getMenu () {
2 // ✅ 创建新副本再包装
3 return Collections .unmodifiableList (new ArrayList <>(products));
4 // new ArrayList<>(products) 创建了副本,和原list无关
5 }
💡 更好的方案: 用 List.of()(Java 9+)或 Guava 的 ImmutableList.copyOf(),它们创建的是真正不可变的副本。
🗂️ 集合选型速查图
集合类型
有序
线程安全
查找复杂度
适用场景
ArrayList
✅ 插入序
❌
O(1) 随机访问
读多写少,遍历为主,大多数场景首选
LinkedList
✅ 插入序
❌
O(n) 随机访问
频繁头尾增删,队列/双端队列场景
HashMap
❌ 无序
❌
O(1) 平均
键值查找,缓存,单线程统计
ConcurrentHashMap
❌ 无序
✅
O(1) 平均
多线程共享Map,并发缓存,计数器
TreeMap
✅ 排序序
❌
O(log n)
需要排序/范围查询,如排行榜、区间查找
HashSet
❌ 无序
❌
O(1) 平均
去重,集合运算(交/并/差),存在性判断
✅ 安全选型口诀
单线程 → ArrayList + HashMap
多线程 → ConcurrentHashMap + CopyOnWriteArrayList
要排序 → TreeMap / TreeSet
❌ 常见踩坑
HashMap多线程put → 死循环/数据丢失
LinkedList频繁get(position) → O(n)极慢
TreeMap的compareTo不一致 → 数据"丢失"
📝 总结 — 集合选型速查表
场景
❌ 错误选择
✅ 正确选择
问题
多线程写Map HashMap ConcurrentHashMap 死循环/数据丢失
可变对象做Key Store对象 String id 内存泄漏
原子性操作 get+判断+put compute/CAS 超卖/库存负数
遍历+修改 ArrayList+for-each CopyOnWriteArrayList CME异常
大数据加载 selectList全查 分批/Cursor OOM
需要随机访问 LinkedList ArrayList/ArrayDeque O(n)慢
排序去重 compareTo只比一字段 多字段比较 数据丢失
返回只读列表 unmodifiable(原list) unmodifiable(new ArrayList) 数据偷偷变了
💡 记忆口诀:
• 多线程共享? → ConcurrentHashMap,别用HashMap
• 复合操作? → compute/cas,别get-then-put
• 遍历时修改? → CopyOnWriteArrayList或加锁
• 大数据? → 分批/游标,别selectList全查
• Map的Key? → 不可变对象(String/Integer),别用可变对象
• 返回只读? → 先copy再包装,别直接包原list
🔥 补充案例 — 更多工作中真实遇到的问题
📌 补充1: HashMap 初始容量不设 — 频繁扩容拖慢性能
🎬 什么时候出:
你知道要往HashMap里放10000条数据,但没指定初始容量。HashMap默认容量16,负载因子0.75。每次到阈值就扩容(16→32→64→128→...),每次扩容都要重新计算所有key的hash、重建数组。放10000条数据要扩容 10次 !
📋 问题代码
📁 service
☕ ProductBatchService
1 // ❌ 知道要放10000条,但不设容量
2 Map <String , Product > map = new HashMap <>();
3 for (Product p : allProducts) { // allProducts.size() = 10000
4 map.put (p.getId (), p); // 16→32→64→128→...→16384 扩容10次!
5 }
16 × 0.75 = 12 → put第13个时扩到32
32 × 0.75 = 24 → put第25个时扩到64
64 × 0.75 = 48 → put第49个时扩到128
128 × 0.75 = 96 → put第97个时扩到256
...
总共扩容 10 次!每次扩容都要重新hash所有已有元素
✅ 修复
1 // ✅ 指定初始容量 = 预期大小 / 0.75 + 1
2 // 10000 / 0.75 = 13333,取2的幂次 = 16384
3 Map <String , Product > map = new HashMap <>(13334 );
4 // 一次扩容都不用!
💡 记忆公式: 如果知道要放N个元素,初始容量设 N / 0.75 + 1(向上取2的幂次),这样就不用扩容。
📌 补充2: synchronizedMap vs ConcurrentHashMap — 性能差很多
🎬 什么时候出:
有人知道HashMap不安全,用了 Collections.synchronizedMap()。虽然线程安全了,但每次get/put都锁整个Map,并发一高就全堵住了。ConcurrentHashMap 用分段锁(Java 7)或 CAS + synchronized(Java 8),只锁一个桶,并发性能好得多。
❌ synchronizedMap — 锁整个Map
1 Map <String ,Integer > map =
2 Collections .synchronizedMap (new HashMap <>());
3 // get → synchronized(map) { ... }
4 // put → synchronized(map) { ... }
5 // 100个线程全部排队等同一把锁!
✅ ConcurrentHashMap — 只锁一个桶
1 Map <String ,Integer > map =
2 new ConcurrentHashMap <>();
3 // get → 无锁读取(volatile)
4 // put → 只锁hash冲突的那个桶
5 // 100个线程可以同时写不同的桶!
Collections.synchronizedMap: 8.5s
ConcurrentHashMap: 1.2s // 快7倍!
📌 补充3: Arrays.asList() 返回的List不能add/remove
🎬 什么时候出:
你用 Arrays.asList() 快速创建了一个List,然后往里add元素,直接 UnsupportedOperationException!因为 Arrays.asList() 返回的是 Arrays 内部的 ArrayList,不是 java.util.ArrayList,它没有实现 add/remove 方法。
1 // ❌ Arrays.asList 返回的是固定大小的List
2 List <String > products = Arrays .asList ("键盘" , "鼠标" , "音箱" );
3 products.add ("机械" ); // 💥 UnsupportedOperationException!
4
5 // ✅ 修复:包装成真正的ArrayList
6 List <String > products = new ArrayList <>(Arrays .asList ("键盘" , "鼠标" , "音箱" ));
7 products.add ("机械" ); // ✅ OK!
📌 补充4: ArrayList.subList() 导致原List无法GC
🎬 什么时候出:
你用 list.subList(0, 10) 取前10个元素,然后用完了subList不要了。但你不知道 subList 返回的是原List的一个"视图",它内部持有着原List的引用!原List有100万条数据,但只通过subList"暴露"了10条,原List无法被GC回收。
1 List <Order > allOrders = orderMapper.selectAll (); // 100万条
2
3 // ❌ subList是视图,持有allOrders的引用
4 List <Order > top10 = allOrders.subList (0 , 10 );
5 allOrders = null ; // 你以为allOrders可以被GC了?
6 // ❌ 不能!top10内部还持有100万条的引用!
7
8 // ✅ 修复:创建新List,断开引用
9 List <Order > top10 = new ArrayList <>(allOrders.subList (0 , 10 ));
10 allOrders = null ; // ✅ 现在allOrders可以被GC了
📌 补充5: HashSet 也是线程不安全的!别在多线程用
🎬 什么时候出:
HashSet 内部就是 HashMap(value是一个固定的Object),所以 HashSet 有 HashMap 一样的问题:并发 add 可能死循环、数据丢失。但很多人不知道,以为 Set 没问题。
❌ 并发用HashSet
1 // ❌ HashSet = new HashMap<>()
2 Set <String > processed = new HashSet <>();
3 // 多线程 add → 和HashMap一样的问题!
✅ 用ConcurrentHashMap.newKeySet
1 // ✅ 线程安全的Set
2 Set <String > processed =
3 ConcurrentHashMap .newKeySet ();
4 // 或者用 CopyOnWriteArraySet