【集合】2、Map相关问题总结

74 阅读5分钟

线程安全

线程安全:ConcurrentHashMap、HashTable、Collections.synchronizedMap(有点像包装类

  • synchronizedMap :synchronizedMap类似于一个包装类,它把传入的map作为一个成员,每个方法都加锁,锁的粒度大。不是完全线程安全的,它可以解决并发put的问题,但并发修改元素时迭代器遍历map仍然会抛出异常。(简单来说就是遍历的时候 修改其中的内容,会不安全,可能是自己线程边遍历边修改)SynchronizedXXX 系列并发容器分析_collections.synchronizedmap iterator报错-CSDN博客

线程不安全:HashMap

HashMap

HashMap和HashTable的区别?多记

线程安全null Key的支持初始化及扩容底层结构 ****四个方面看。

  • HashMap线程不安全,HashTable安全
  • HashMap可以有null的key和value,HashTable键值都不能为null
  • HashMap初始化16扩容乘2,HashTable初始化11扩容乘2+1
  • 1.8之后HashMap当链表长度大于8且数组长度大于64 时链表会转换为红黑树。HashTable不会变。

HashMap和HashSet的区别?

HashSet底层就是基于HashMap实现的,往HashSet中add元素E,其实就是向内部的map中put一个以E为key,一个固定的Object为value的键值对。我的理解: HashSet有点像HashMap的一个装饰器类,大部分操作都是对map进行操作的。

构造函数

add方法

contains方法

remove方法

HashMap与TreeMap的区别?

但是需要注意的是TreeMap它还实现了NavigableMap接口和SortedMap 接口

实现 NavigableMap 接口让 TreeMap 有了对集合内元素的搜索的能力。如何搜索?

实现SortedMap接口让 TreeMap 有了对集合中的元素根据键/key排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器Comparator

综上,相比于HashMap来说 TreeMap 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。

HashSet如何检查重复?(记一下,可以记一下代码)

HashSet的add时调用HashMap的put(),而map中无论是否已经存在key,其实都会put成功(如果key已经存在就是替换),那HashSet如何检查的?

其实HashMap的put方法有返回值,如果插入key位置不存在元素返回null,存在元素返回上一个元素。

而HashSet只是在add返回的时候,告诉我们插入前是否存在元素(map.put的返回值是否为null)

为null返回true说明之前不存在元素,不为null返回false说明之前存在元素。

HashMap 的长度为什么是 2 的幂次方 ?

首先元素在数组中存储的位置是通过hash值对长度取余数得到的(hash%length),然而取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的 前提是 length 是 2 的 n 次方 ;), 所以说如果长度是2的幂次方,那么可以用位运算的方式计算即取余操作为 (n - 1) & hash, 比用%取余的效率高多了。

HashMap 的底层实现

从 底层结构,初始化和扩容 说

通过key的hashCode的值再经过 扰动函数 得出的hash值, hash值 对数组取余计算出存储位置。 (扰动函数防止实现较差的hashcode方法碰撞太多)

1.8之前entry数组+链表。数组中存的entry节点,每个entry中key、value和next

1.8及之后:当链表长度大于8且数组长度大于64,链表转换为红黑树。如果数组长度没有64,那么先扩容。

HashMap的遍历方式?

你一般是怎么遍历HashMap的?

  1. 通过EntrySet遍历(分为forEach 和 迭代器遍历)
  2. 通过ketSet遍历(分为forEach 和 迭代器遍历)
  3. 1.8中 lambda表达式+forEach遍历
// 1.8lambda表达式
map.forEach((key, value)->{
    System.out.println(key +": "+value);
});

concurrentHashMap

ConcurrentHashMap 和 Hashtable 的区别

底层数据结构实现线程安全的方式来说,ConcurrentHashMap则有JDK1.7JDK1.8的区别

底层数据结构

Hashtable 数组加链表。

ConcurrentHashMap: 1.7及以前,分段数组+entry数组+链表。1.8及以后,数组+链表/红黑树(与HashMap相同)

加锁方式

Hashtable:synchronized加到方法上,导致所有同步方法上锁(对象锁)。

ConcurrentHashMap:

1.7及以前,对每个Segment加锁,一个Segment包含一个entry数组。

1.8及以后

ConcurrentHashMap 取消了 Segment 分段锁,采用 Node + CAS + synchronized 来保证并发安全。数据结构跟 HashMap 1.8 的结构类似,数组+链表/红黑二叉树。

Java 8 中,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发。

ConcurrentHashMap 线程安全的具体实现方式/底层具体实现

具体实现方式

1.8之前:segment数组+entry数组+链表,对segment段加锁,并发线程可以访问不同段中的数据。

每个segment类似于小的hashMap,对增删改操作时,确定元素的segment然后对segment加锁。

使用 ReentrantLock自旋获取Segment锁,因为Segment是ReentrantLock的子类

1.8及之后:Node数组 + 链表/红黑树,对Node首节点加锁

添加元素时首先会判断容器是否为空:的.添加元素时首先会判断容器是否为空:

  • 如果为空则使用volatile (有一个sizectl变量表示初始化状态)加CAS 来初始化
  • 如果容器不为空,则根据存储的元素计算该位置是否为空(数组中的节点)
    • 如果计算该位置结果为空,则利用CAS设置该节点;
    • 如果计算该位置结果不为空,则使用synchronized锁住第一个节点 ,然后,遍历桶中的数据,并替换或新增节点到桶中,最后再判断是否需要转为红黑树,这样就能保证并发访问时的线程安全了。

总结,使用volatile关键字+cas/synchronized锁实现。

ConcurrentHashMap支持null 键和值吗?为什么