Map结构的常用以及了解

·  阅读 208

@TOC

1、经常使用哪个Map结构

平时用到最多的几种形式

Map<String, Integer> curMap = new ConcurrentHashMap<String, Integer>();
Map<String, Object> hMap = Maps.newHashMap();
Map<String, Integer> curMap = Maps.newConcurrentMap();
复制代码

那么Map与HashMap、ConcurrentMap之间有什么关系呢? Map是原始的接口,AbstractMap是Map接口的抽象类,HashMap、ConcurrentHashMap是AbstractMap的实现类。

2、这些Map结构有啥区别

①、HashMap是我们最常用的键值对的存储结构,占据平时使用场景的90%,但是有时候操作多个线程进行数据查询并且最终进行数据汇总的时候使用HashMap就不可以了,这也是我们都知道的HashMap是线程不安全的原因
②、HashMap是怎么存储键值对的呢?
HashMap存储Key-Value的时候使用的是数据+链表的形式存储,将Key值hash之后计算出一个index值,这个index代表元素落入数据的哪个位置,可能存在多个key的hash值是一样的,这种冲突情况下,会在该数组元素后面,加一个链表,解决冲突。(小知识:解决hash冲突有几种办法呢?1、开放地址法;链地址法)
③、HashMap初始化的时候需要设置大小吗? HashMap的初始化函数有以下几个:

  public HashMap(int initialCapacity, float loadFactor) { // initialCapacity 初始容量,loadFactor负载因子
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    
   public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
   }
   
   public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
   }
   
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
复制代码

从这几个构造函数来看,最主要初始化的变量是: initialCapacity、 loadFactor 创建的时候不设置大小会有一个问题,就是元素刚进来就会有一次扩容
loadFactor默认值是0.75
④、HashMap存储的数据量很大时候怎么扩容呢,一般是什么时候进行扩容呢?
resize有两种情况,a、调用put方法刚开始执行时判断当前table是否等于null, ⑤、HashMap为什么是线程不安全的呢?

3、HashMap的底层实现原理,怎么存储的数据

HashMap的put方法

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 如果table为空
        if ((tab = table) == null || (n = tab.length) == 0)
            // 直接进行扩容
            n = (tab = resize()).length;
        // 根据(n-1)&hash算法之后确定数组的下标,此处值为空的话,直接新建一个节点
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        // 此处值不为空(存在了hash冲突)
        else {   
            Node<K,V> e; K k;
            // hash到该位置,并且这个节点的值与传入的value值相等,则直接进行节点内容替换
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 如果是红黑树节点,直接添加到红黑树
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // key值不相同
                for (int binCount = 0; ; ++binCount) {
                    // 节点下的链表没有值与传入的值相等,则需要创建新节点
                    if ((e = p.next) == null) {
                        // 新建一个节点,放入key、value
                        p.next = newNode(hash, key, value, null);
                        // 这个节点下链表的大小大于8
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            // 转换为红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 遍历链表的时候如果遇到了节点key、value都相等的节点,直接退出
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    // p往后走一个链表节点    
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 操作记录数+1
        ++modCount;
        if (++size > threshold)
            // map的节点数大于最大限定值,触发扩容
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    
复制代码

4、线程安全方面

jdk7环境下resize可能会导致死循环

  //传入新的容量
  void resize(int newCapacity) {   
       //引用扩容前的Entry数组
      Entry[] oldTable = table;   
      int oldCapacity = oldTable.length; 
      //扩容前的数组大小如果已经达到最大(2^30)了
      if (oldCapacity == MAXIMUM_CAPACITY) {  
          //修改阈值为int的最大值(2^31-1),后期也不会再扩容了
          threshold = Integer.MAX_VALUE; 
          return;
      }
   
      //初始化一个新的Entry数组
      Entry[] newTable = new Entry[newCapacity];  
      //将数据转移到新的Entry数组里<重点>
      transfer(newTable);      
      //HashMap的table属性引用新的Entry数组
      table = newTable;               
      //修改阈值
      threshold = (int)(newCapacity * loadFactor);
 }
 
 //将数据转移到新的Entry数组里
 void transfer(Entry[] newTable) {
      //src引用了旧的Entry数组
      Entry[] src = table;                  
      int newCapacity = newTable.length;
      //遍历旧的Entry数组
      for (int j = 0; j < src.length; j++) { 
          //取得旧Entry数组的每个元素
          Entry<K,V> e = src[j];             
          if (e != null) {
              //释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
              src[j] = null;
              do {
                 Entry<K,V> next = e.next;
                 //!!重新计算每个元素在数组中的位置
                 int i = indexFor(e.hash, newCapacity); 
                 //标记[1]
                 e.next = newTable[i]; 
                 //将元素放在数组上<头插法>
                 newTable[i] = e;      
                 //访问下一个Entry链上的元素
                 e = next;             
             } while (e != null);
         }
     }
 } 
复制代码

线程1挂起在newTable[i] = e处,线程2执行完resize之后map已经变成如下:

线程1还处于: 此时线程1中3的后面指向7,但是线程2扩容之后7的后面指向3,所以此处就会存在环形链表的情况导致死循环

分类:
后端
标签: