Android知识点15--HashMap/LinkedHashMap/concurrentHash原理

43 阅读5分钟

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的幂指数作为实际并发度。