List、Set、Map

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对比

特性ListSet
查找性能contains()是O(n),需要遍历contains()是O(1),哈希查找
有序性保持插入顺序HaseSet不保证顺序
重复元素允许重复自动去重
内存占用相对较小相对较大(哈希表结构)

使用Set的优缺点(相对于List)

  • 优点
    • 查找效率高:contains()是O(1),List是O(n)
    • 自动去重:防御性编程,避免脏数据导致逻辑异常
  • 缺点
    • 失去了原始数据的顺序信息(依据业务选择,如果不在乎顺序,则可以用Set)
    • 多占用一些内存(若数据量可控,则性能收益更大)

二、HashMap vs HashSet对比

特性HashMapHashSet
存储内容键值对(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的优点

  1. 语义清晰:contains()直接表达“是否包含”的意图
  2. 代码简洁:不需要维护无意义的value
  3. 内存更省:HashSet内部虽然也是用HashMap实现,但value统一是同一个PERSENT静态对象,比每次存Boolean更省内存
  4. 仅判断存在性的需求,Set更适合

总结

场景推荐选择
仅需判断元素是否存在HashSet
需要通过key找到对应valueHashMap
需要去重并保持顺序LinkedHashSet
需要排序TreeSet/TreeMap

三、线程安全

  1. ConcurrentHashMap.newKeySet() (推荐)
    1. Set<String> set = ConcurrentHashMap.newKeySet();基于ConcurrentHashMap的newKeySet
    2. 线程安全,性能高(分段锁)
    3. 最推荐用于高并发场景
  2. Collections.synchronizedSet
    1. Set<String> set = Collections.synchronizedSet(new HashSet<>());
    2. 包装器模式,对所有方法加synchronized
    3. 线程安全,但并发性能较低(全局锁)
  3. CopyOnWriteArraySet
    1. Set<String> set = new CopyOnWriteArraySet<>();
    2. 写时复制,读操作无锁
    3. 适合读多写少的场景
    4. 写入开销大(需要复制整个集合)
  4. ConcurrentHashMap代替(如果需要更多操作)
    1. ConcurrentHashMap<String ,Boolean> map = new ConcurrentHashMap<>();
    2. 判断存在性用containsKey 或 putIfAbsent
  5. 对比总结
实现方式线程安全机制适用场景性能特点
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对比

特性HashMapConcurrentHashMap
线程安全非线程安全线程安全
锁机制无锁分段锁(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值策略

HashMapConcurrentHashMap
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性能对比

场景HashMapConcurrentHashMap
单线程put/get最优略慢(有额外并发控制开销)
多线程并发读不安全极高(读基本无锁)
多线程并发写不安全高(分段锁,并行写入)
混合读写不安全

5.6选择建议

场景推荐选择
方法内局部变量,单线程使用HashMap
类成员变量,多线程共享ConcurrentHashMap
需要排序TreeMap/ConcurrentSkipListMap
需要缓存+过期策略Caffeine/Guava Cache