不能直接用hashset存关注列表,因其非线程安全,并发增删易丢数据或抛concurrentmodificationexception;应使用collections.synchronizedset或concurrenthashmap.newkeyset()。

Map> 为什么不能直接用 HashSet 存关注列表
HashSet 存关注列表因为 HashSet 不保证线程安全,而社交关系的增删(比如“关注”“取关”)常并发发生。你用 new HashMap() 配 new HashSet(),看似能跑通,但一旦两个请求同时对同一个用户的关注集合调 add() 和 remove(),就可能丢数据或抛 ConcurrentModificationException。
实操建议:
- 关注集合统一用
Collections.synchronizedSet(new HashSet())包一层,或者更推荐——直接用ConcurrentHashMap.newKeySet()(Java 8+),它底层是分段锁,性能更好 -
Map层别偷懒用HashMap,必须换成ConcurrentHashMap,否则 putIfAbsent、computeIfAbsent 这类原子操作没法保障 - 别在循环遍历
Set时修改它,哪怕加了同步也不行;要改就先转成新集合再替换,例如:userFollows.replace(userId, Collections.unmodifiableSet(newFollowSet))
computeIfAbsent 是不是万能的“懒加载”写法
不是。它只保证“键不存在时创建并放入”,但不保证“创建出来的值本身线程安全”。比如写 map.computeIfAbsent(uid, k -> new HashSet()),多个线程同时触发,可能生成多个 HashSet 实例,只留一个进 map,其余被 GC——看起来没毛病,但后续对这个 Set 的操作还是非线程安全的。
实操建议:
- 把集合初始化逻辑和线程安全绑定:用
computeIfAbsent(uid, k -> ConcurrentHashMap.newKeySet()) - 如果 JDK 版本低于 8,老老实实用双重检查 +
synchronized(map)块,别图省事 - 注意
computeIfAbsent的 lambda 不能有副作用(比如发通知、写日志),它可能被多次执行(虽然最终只存一个)
“互相关注”判断为什么不能只查两次 contains
因为 a-follows-b 和 b-follows-a 是两条独立路径,中间可能有时间差。比如 A 关注 B 后,B 还没点确认(如果是双向关注模型),或 B 刚取关 A,但 A 的缓存还没刷新——这时候单纯查 follows.get(a).contains(b) && follows.get(b).contains(a),结果不稳定,且容易误判。
实操建议:
- 业务上需要“互关”状态,就单独建一张
mutual_follow表或 Redis 的SET,由关注/取关事件驱动更新,不要实时计算 - 如果必须实时判断,至少加读锁或用
StampedLock读模式包裹两次查询,避免中间态被修改 - 测试时重点模拟“关注→取关→再关注”的毛刺场景,观察是否出现短暂的单向/双向状态错乱
内存暴涨是不是因为 String 用户 ID 没复用
是。如果用户 ID 来自 HTTP 参数或 JSON 解析,每次都是新 String 实例,Map 的 key 就无法复用,导致大量重复字符串对象堆积。10 万用户,每个 ID 平均 12 字节,光 key 就多占几 MB 内存,加上每个 ConcurrentHashMap 的桶数组和 KeySet 开销,很容易 OOM。
实操建议:
- 所有外部进来的用户 ID,第一时间调
intern()(前提是 ID 稳定、总量可控);或用String.valueOf(id).intern()避免空指针 - 更稳妥的做法:用
Long或Integer当用户主键,ID 映射走数据库或本地缓存,Map 的 key 改成数字型,省空间又快 - 用
jmap -histo定期看堆里java.lang.String实例数,超过用户量 2 倍就要查来源
嵌套结构看着简单,但 Map 的并发策略、Set 的实例生命周期、字符串的内存驻留——这三个地方任何一个没抠准,线上跑一周就可能出问题。









