1. HashMap的原理是什么?为什么不安全?
1.7版本: Table数组+Entry链表 1.8版本: Table数组+Entry链表/红黑树 变量参数:
- Table数组默认初始化长度为 16
- Table数组的最大长度为 1<<30 2^30次
- 默认负载因子为0.75,扩容时数组扩容到原来的两倍
- 链表转换成树的阈值为8. 红黑树转换成链表的阈值为6.
class HashMap {
// table数组
transient Node<K,V>[] table;
// 数组中每个元素实现了Map.Entry<K,V>
static class Node<K,V> implements Map.Entry<K,V> {}
}
HashMap底层数组也可以使用LinkedList来实现,效率没有数组快,ArrayList的扩容倍数是1.5倍。 HashMap对保存的key进行hashCode(),然后对数组长度进行取余运算,得到在数组中插入的位置。
HashMap.put()方法
- 对key的hashCode()做hash运算,计算index
- 如果没有碰撞直接放到bucket中
- 如果出现了碰撞,放在链表或者红黑树中
- 如果节点已经存在就替换oldValue(key是唯一的)
- 如果元素超过一定阈值,则进行扩容resize
HashMap.resize()方法 扩容机制就是将老的table数组中所有Entry取出来,重新散列到新的table中。
HashMap.get()方法
- 对key的hashCode()做hash运算,计算索引值index
- 如果在bucket中第一个节点就命中,直接返回
- 如果不在第一个,则在链表(O(n))或红黑树中(O(logn))查找
1.8相对于1.7,hashMap修改了什么?
- 数组+链表 修改为 数组+链表+红黑树
- 优化了高位运算的hash算法: h^(h>>>16)
- 扩容后,元素的位置在原位置或者原位置移动2次幂的位置,这样就不会出现死循环问题了
红黑树操作需要进行左旋、右旋、变色操作来保持平衡,当元素个数小于8时用单链表操作比较合适。
HashMap的并发问题?
- 多线程扩容时,会引起死循环问题
- 多线程put的时候,可能会导致元素丢失
- put非null元素后get出来的却是null
HashMap的key有什么要求? 一般来说使用Integer/String这类不可变类当做HashMap的key,String最常用,字符串在创建的时候它的hashCode就被缓存了,不需要重新计算。获取的对象要用到equals()和hashCode()方法,不可变类已经比较很好实现了这两个方法。 如果用可变类,hashCode的值可能会变化。
四个原则
- 两个Object相同,hashCode一定相等
- 两个Object不同,hashCode不一定不等
- hashCode相同,两个Object不一定相等
- hashCode不同,两个Object一定不等
如何实现一个不可变类来作为HashMap的key?
- 使用final修饰class
- 使用private final 修饰所有成员变量
- 没有修改成员变量的方法
- 通过构造器初始化所有成员,进行深拷贝
- getter方法中返回对象的克隆
HashMap的key可以为null, ConcurrentHashMap的key不能为null。 为什么concurrentHashMap的key不能为空呢? // zhuanlan.zhihu.com/p/162996476 假设有两个线程A和B,假设key没被映射过,A线程get(key)后再调用containsKey(key)之前,B线程执行了put(key,null),那么A线程执行完containsKey(key)方法拿到的结果就是true了,与预期结果不相符。
2. LinkedHashMap的原理是什么?
LinkedHashMap继承HashMap,实现了Map<K,V>接口。 在HashMap中元素是无序的,而LinkedHashMap使用双端队列来存储元素,可以保证插入顺序。 在LinkedHashMap的Entry类继承自HashMap.Entry<K,V>,在这个基础上又添加了after和before属性,分别指向下一个和上一个节点。 accessOrder属性用于描述迭代的顺序,为true则为访问顺序,为false则为插入顺序,默认为false也就是输出的顺序为插入顺序。 LinkedHashMap可以保证数据的有序性,但是不会对元素排序,可以使用TreeMap来对元素进行排序,
Map<String, Integer> treeMap = new TreeMap<>(Comparator.naturalOrder());
treeMap.put("One", 1);
treeMap.put("Two", 2);
treeMap.put("Three", 3);
treeMap.put("Four", 4);
treeMap.put("Five", 5);
System.out.println("treeMap内容:");
for (Map.Entry<String, Integer> entry : treeMap.entrySet()) {
System.out.println(entry);
}
List<Map.Entry<String, Integer>> list = new ArrayList<>(treeMap.entrySet());
list.sort((o1, o2) -> o2.getValue().compareTo(o1.getValue()));
System.out.println("排序后treeMap内容:");
for (Map.Entry<String, Integer> e : list) {
System.out.println(e.getKey() + ":" + e.getValue());
}
3. concurrentHash的原理是什么?
jdk1.7版本使用段(Segment)来表示不同的部分,只要多个修改发生在不同的段上,他们就可以并发进行,Segment中存放的是HashEntry[]。定位一个元素需要两个hash,第一次hash定位到Segment,第二次hash定位到元素所在的链表头部。 jdk1.8版本降低了锁的粒度为HashEntry(首节点)。采用CAS无锁算法+Synchronized锁。
- concurrentHashMap中变量使用final和violatile修饰的作用? final可以保证初始化安全性,让不可变对象不需要同步就能自由地被访问和共享。 violatile可以保证某个变量的内存改变对其他线程即使可见,在配合CAS可以实现不加锁对并发操作的支持。
- concurrentHashMap有什么缺陷? 非阻塞,更新数据时不会将整个表锁住,严格来说读取操作并不是保证获取最新的更新。
static class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
final float loadFactor;
Segment(float lf) { this.loadFactor = lf; }
}
concurrentHashMap的并发数默认是16,这个值可以在构造函数中设置。如果设置了17,那么实际并发读是32.它以大于等于该值的最小的2的幂指数作为实际并发度。