【面试篇】数据结构-哈希表
【面试篇】HashMap常见面试题目
【面试篇】HashMap1.7和HashMap1.8的详细区别对比
【面试篇】ConcurrentHashMap1.8 扩容细节
【面试篇】ConcurrentHashMap1.7和1.8详解对比
什么是一致性哈希算法?如何通俗易懂的了解分布式缓存场景?
1.Java容器有哪些?
Java容器类库定义了两个不同概念的容器:Collection和Map
a.Collection
一个独立元素的序列,这些元素都服从一条或多条规则。List必须按照插入的顺序保存元素;Set不能有重复元素;Queue按照排队规则来确定对象产生的顺序。
b.Map
一组成对的“键值对”对象,允许使用键来查找值
2.HashMap的常见题目
HashMap = 数组+链表+红黑树
HashMap是基于Hash算法实现的,通过put(key,value)来存储,get(key)来获取。当传入key时mhashmap会根据key,通过hashCode()计算出hash值,根据hash值将value保存在bucket中。
a.哈希冲突
当2个不同的key,经过哈希函数计算出相同的结果时。
解决措施:
- 开放定址法:按照一定规则向其它地址探测,直到遇到空桶;
- 再哈希法:设计多个哈希函数
- 链地址法:通过链表将其串起来
Java的JDK1.8解决措施:
HashMap 的做法是用链表和红黑树存储相同 hash 值的 value。当 hash 冲突的个数比较少时,使用链表否则使用红黑树。
b.HashMap数据插入
-
判断数组是否为空,为空进行初始化;
-
不为空,计算 k 的 hash 值,通过
(n - 1) & hash
计算应当存放在数组中的下标 index; -
查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在 table[index] 中;
-
存在数据,说明发生了hash冲突(存在二个节点key的hash值一样), 继续判断key是否相等,相等,用新的value替换原数据(onlyIfAbsent为false);
-
如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;(如果当前节点是树型节点证明当前已经是红黑树了)
-
如果不是树型节点,创建普通Node加入链表中;判断链表长度是否大于 8并且数组长度大于64, 大于的话链表转换为红黑树;
-
插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍。
c.HashMap的初始化
如果HashMap()不传值,默认大小是16,负载因子是0.75.
衡量hashMap需要扩容的条件是:hashMap.size >=capacity x loadfactor
d.HashMap的哈希函数怎么设计的?
hash函数需要先拿到key的hashCode,是一个32位的int值,然后让hashCode的高16位和低16位进行异或操作,然后再与(hashMap.length-1)进行与运算,得到bucket值。
扰动计算:设计原因:
- 一个好的哈希函数要尽可能降低hash碰撞,越分散越好
- 算法要尽可能高效,所以用位运算。
防止不同hashCode的高位不同但低位相同导致的hash冲突。简单点说,就是为了把高位的特征和低位的特征组合起来,降低哈希冲突的概率,也就是说,尽量做到任何一位的变化都能对最终得到的结果产生影响。
e.java 1.8对hashmap的优化
- 数组+链表修改成了数组+链表或红黑树:防止链表长度过长,将时间复杂度由O(n)降为O(logn);
- 链表的插入方式从头插法改为尾插法:因为在1.7头插法扩容时,头插法会时链表发生反转,多线程环境下会产生环。
-
在插入时,1.7线判断是否扩容,再插入;在1.8是先进行插入,插入完成再判断是否还需要扩容
-
java1.7扩容的时候需要对原数组中的元素进行重新hash定位在新数组的位置;而java1.8采用更简单的判断逻辑。
A:扩容后,若hash值新增参与运算的位=0,那么元素在扩容后的位置=原始位置
B:扩容后,若hash值新增参与运算的位=1,那么元素在扩容后的位置=原始位置+扩容后的旧位置。
f.那hashMap是线程安全的吗?
hashMap是线程不安全的。在java1.7中hashMap会产生死循环、数据丢失、数据覆盖的问题;而在java1.8中会有数据覆盖的问题。以1.8为例,当A线程判断index位置为空后正好挂起,B线程开始往index位置的写入节点数据,这时A线程恢复现场,执行赋值操作,就把A线程的数据给覆盖了;还有++size这个地方也会造成多线程同时扩容等问题。
HashMap
的线程不安全主要体现在下面两个方面:
1.在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。
2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。
g.如何解决hashMap的线程不安全的问题?
-
使用hashtable来替代hashmap
hashtable是直接在数组上架synchronized关键字,锁住了整个数组。例如:当一个线程使用put方法时,另外一个线程不但不可以使用put方法,连get方法都不可以,效率很低,一般不会使用。
-
使用Collections类的synchronizedMap(Map<K,V> m)方法可以返回一个线程安全的Map
例如:通过传入Map之后封装出一个SynchronizedMap对象
Map<String, Integer> crunchifySynchronizedMapObject = Collections.synchronizedMap(new HashMap<String, Integer>());
-
使用ConcurrentHashMap来定义Map
其使用分段锁,降低了锁粒度,并发度大大提高。例如package java.util.concurrent;
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
…
}
h.ConcurrentHashMap的分段锁原理?
在ConcurrentHashMap中成员变量使用volatile修饰,免除了指令重排序,同时保证了内存可见性;
使用了CAS操作和synchronized结合实现赋值操作。
多线程操作只会操作当前操作索引的节点。
i.hashMap内部节点是无序的,那么有没有有序的Map?
LinkedHashMap和TreeMap。
- TreeMap:是按照key的自然顺序或者compartor的顺序进行排序,内部是通过红黑树来排序。所以要么key所属的类实现了comparable接口,要么自定义一个实现了comparator接口的比较器,传给treemap用于key的比较。
- LinkedHashMap:是在hashmap的基础上,在内部维护了一个单链表,有头尾节点before和after来标识前置节点和后置节点,可以用来实现按插入的顺序或访问的顺序排序。
j. hashset的实现原理
hashset是基于hashmap实现的,hashset底层使用hashmap来保存所有元素。hashset不允许重复的值。