List、Set、Map
记一次List<String>转换Set<String>的代码实现及其扩展。
List<String> list = Arrays.asList("aaa","bbb","ccc"); //第一种方式 Set<String> set = list.stream().collect(Collectors.toSet()) //第二种方式 Set<String> set2 = new HashSet<>(); if(list!=null){ set2.addAll(list); }一、List vs Set对比
| 特性 | List | Set |
| 查找性能 | contains()是O(n),需要遍历 | contains()是O(1),哈希查找 |
| 有序性 | 保持插入顺序 | HaseSet不保证顺序 |
| 重复元素 | 允许重复 | 自动去重 |
| 内存占用 | 相对较小 | 相对较大(哈希表结构) |
使用Set的优缺点(相对于List)
- 优点
- 查找效率高:contains()是O(1),List是O(n)
- 自动去重:防御性编程,避免脏数据导致逻辑异常
- 缺点
- 失去了原始数据的顺序信息(依据业务选择,如果不在乎顺序,则可以用Set)
- 多占用一些内存(若数据量可控,则性能收益更大)
二、HashMap vs HashSet对比
| 特性 | HashMap | HashSet |
| 存储内容 | 键值对(Key-Value) | 仅存储元素(Key) |
| 底层实现 | 数组+链表/红黑树 | 内部封装了一个HashMap |
| 用途 | 需要通过Key查找Value | 仅需判断元素是否存在 |
| 内存占用 | 较大(存储K+V) | 相对较小(只存K,V是一个固定的Object) |
| 常用方法 | put(),get(),containsKey() | add(),contains() |
| 是否允许null | 允许一个null key,多个null value | 允许一个null元素 |
2.1选择HashSet的优点
- 语义清晰:contains()直接表达“是否包含”的意图
- 代码简洁:不需要维护无意义的value
- 内存更省:HashSet内部虽然也是用HashMap实现,但value统一是同一个PERSENT静态对象,比每次存Boolean更省内存
- 仅判断存在性的需求,Set更适合
总结
| 场景 | 推荐选择 |
| 仅需判断元素是否存在 | HashSet |
| 需要通过key找到对应value | HashMap |
| 需要去重并保持顺序 | LinkedHashSet |
| 需要排序 | TreeSet/TreeMap |
三、线程安全
- ConcurrentHashMap.newKeySet() (推荐)
- Set<String> set = ConcurrentHashMap.newKeySet();基于ConcurrentHashMap的newKeySet
- 线程安全,性能高(分段锁)
- 最推荐用于高并发场景
- Collections.synchronizedSet
- Set<String> set = Collections.synchronizedSet(new HashSet<>());
- 包装器模式,对所有方法加synchronized
- 线程安全,但并发性能较低(全局锁)
- CopyOnWriteArraySet
- Set<String> set = new CopyOnWriteArraySet<>();
- 写时复制,读操作无锁
- 适合读多写少的场景
- 写入开销大(需要复制整个集合)
- ConcurrentHashMap代替(如果需要更多操作)
- ConcurrentHashMap<String ,Boolean> map = new ConcurrentHashMap<>();
- 判断存在性用containsKey 或 putIfAbsent
- 对比总结
| 实现方式 | 线程安全机制 | 适用场景 | 性能特点 |
| ConcurrentHashMap.newKeySet | 分段锁 | 高并发读写 | 读写性能都高 |
| Collections.synchronizedSet | 全局锁 | 低并发 | 并发度低 |
| CopyOnWriteArraySet | 写时复制 | 读多写少 | 读极快,写极慢 |
6. 不需要线程安全的情况
- 方法内的局部变量
- 没有多线程共享访问
- 单线程内完成构建和使用
- 如果作为类成员变量或缓存被多线程访问,才需要用线程安全的方式
四、ConcurrentHashMap分段锁
4.1什么是分段锁(Segment Lock)
分段锁是ConcurrentHashMap的核心并发机制,它将整个哈希表分成多个独立的段(Segment),每个段时一把独立的锁。
传统HashTable:全局锁[整个哈希表] ->所有操作串行
ConcurrentHashMap:分段1|分段2|分段3|...
锁A|锁B|锁C|...
不同分段的操作可以并行执行
4.2JDK7 vs JDK8的实现差异
| 版本 | 分段锁实现 | 段数量 |
| JDK7 | 显式Segment类,继承ReentrantLock | 默认16个 |
| JDK8 | 取消Segment,直接用synchronized+CAS | 更细粒度(桶级别) |
JDK8的优化:锁的粒度从“段”细化到“单个桶(链表头节点)”,并发度更高
4.3为什么适合高并发
1.并发度提升
- 假设默认16个分段
- 理想情况下,16个线程可以同时操作不同分段的数据
- 对比:
- HashTable:1个线程能操作
- Collections.synchronizedMap:1个线程能操作
- ConcurrentHashMap:16个线程能同时操作
2.读操作基本无锁
//get操作不需要加锁,使用volatile保证可见性 public V get(Object key){ Node<K,V>[] tab;Node<K,V> e,p; int n,eh;K ek; int h = spread(key.hashCode()); //直接定位桶,volatile读取 if((tab=table)!=null && (n=tab.length)>0 && (e=tabAt(tab,(n-1) & h))!=null){ // ... 遍历链表或红黑树 } return null; }
3.写操作细粒度加锁
//put操作只锁住对应的桶(链表头节点) final V putVal(K key,V value,boolean onlyIfAbsent){ //计算hash,定位到具体桶 int hash = spread(key.hashCode()); int binCount = 0; for(Node<K,V>[] tab = table;;){ Node<K,V> f;int n,i,fhl //... //只锁住这个桶的头节点 f synchronized (f) { //在这个桶内操作(插入、链表转红黑树等 } } }4.4核心优化技术
| 技术 | 作用 |
| CAS | 无锁初始化、计数等操作 |
| volatile | 保证数组和节点的可见性 |
| synchronized(细粒度) | 只锁单个桶,而非整个表 |
| 红黑树 | 链表过长时转为树,O(n) -> O(log n) |
| ForwardingNode | 扩容时线程可以协助迁移数据 |
4.5对比其他线程安全Map
| 实现 | 锁机制 | 并发度 | 适用场景 |
| HashTable | 全局synchronized | 低 | 已淘汰 |
| Collections.synchronizedMap | 全局synchronized | 低 | 简单场景 |
| ConcurrentHashMap | 分段/桶级锁 | 高 | 高并发读写 |
| ConcurrentSkipListMap | 无锁(CAS) | 高 | 需要排序 |
4.6总结
分段锁的核心思想时“锁细化”
- 将“一把大锁”拆成“多把小锁”
- 不同分段的数据操作互不干扰
- 读基本无锁,写只锁局部
- 从而实现高并发下的高性能
五、HashMap vs ConcurrentHashMap对比
| 特性 | HashMap | ConcurrentHashMap |
| 线程安全 | 非线程安全 | 线程安全 |
| 锁机制 | 无锁 | 分段锁(JDK7)/桶级锁(JDK8) |
| 性能 | 单线程下最优 | 高并发下最优 |
| null支持 | 允许null key和null value | 不允许null key和null value |
| 迭代器 | fail-fast(快速失败) | 弱一致性,不会抛ConcurrentModificationException |
| 适用场景 | 单线程、局部变量 | 多线程共享、高并发 |
| 内存占用 | 较小 | 略大(维护并发控制结构) |
核心差异
5.1线程安全形
//HashMap - 多线程下会出问题 Map<String,Object> map = new HashMap<>(); //并发put 可能导致:数据丢失、死循环(JDK7)、size不准确 //ConcurrentHashMap - 线程安全 Map<String,Object> cmap = new ConcurrentHashMap<>(); //并发操作安全,数据一致性保证5.2锁粒度对比
| 集合 | 锁 |
| HashMap | 无锁 |
| HashTable | 全局锁 所有操作串行 |
| Collections.synchronizedMap | 全局锁 所有操作串行 |
| ConcurrentHashMap | [桶1锁][桶2锁][桶3锁] 不同桶的操作可以并行,16+线程可同时操作 |
5.3null值策略
| HashMap | ConcurrentHashMap | |
| null key | 允许1个 | 不允许(抛NullPointerException) |
| null value | 允许多个 | 不允许(抛NullPointerException) |
为什么ConcurrentHashMap不允许null?
//并发场景下,无法区分“key 不存在”和“key存在但value为null”
get(key) 返回nll时,语义模糊
concurrentHashMap.get("key");//返回null,是key不存在?还是value就是null?
5.4迭代器行为
//HashMap - fail-fast,并发修改会抛异常 Map<String,String> hashMap = new HashMap<>(); hashMap.put("a","1"); Iterator<String> it = hashMap.keySet().iterator(); hashMap.put("b","2");//修改了map it.next();//抛出ConcurrentModificationException //ConcurrentHashMap - 弱一致性,不会抛异常 Map<String,String> cMap = new ConcurrentHashMap<>(); cMap.put("a","1"); Iterator<String> it2 = cMap.keySet().iterator(); cMap.put("b","2");//修改了map it2.next();//正常执行,可能读到也可能读不到“b”5.5性能对比
| 场景 | HashMap | ConcurrentHashMap |
| 单线程put/get | 最优 | 略慢(有额外并发控制开销) |
| 多线程并发读 | 不安全 | 极高(读基本无锁) |
| 多线程并发写 | 不安全 | 高(分段锁,并行写入) |
| 混合读写 | 不安全 | 高 |
5.6选择建议
| 场景 | 推荐选择 |
| 方法内局部变量,单线程使用 | HashMap |
| 类成员变量,多线程共享 | ConcurrentHashMap |
| 需要排序 | TreeMap/ConcurrentSkipListMap |
| 需要缓存+过期策略 | Caffeine/Guava Cache |
