Java之Map家族

Map成员
  1. HashMap的详解
  2. LinkedHashMap的介绍
  3. TreeMap的介绍
  4. HashTable与HashMap的区别
  5. ConcurrentHashMap的详解

基于JDK1.8介绍(JDK1.7)

1.HashMap的详解

  • 结构:数组+链表+红黑树
  • 原理:HashMap是基于hashing的原理,通过put(k,v)存储对象到HashMap中,通过get(k)方式获取对象。当使用put(k,v)传递key、value的时候,首先调用hashCode()方法,计算,bucket位置来存储Node对象。
    • put(k,v)过程:
      • 对key求Hash值,计算下标。
      • 如果没有碰撞存入桶中,碰撞则放入bucket的链表或者红黑树中。
      • 如果链表超过阈值(默认链表数超过8,总Entry数超过64)则转换为红黑树,链表长度<6则转换回链表。
      • key结点存在则替换旧值。
      • 如果桶满(容量*加载因子),就要resize(扩容2倍后重排)。
    • get(k)过程:
      • 首先将 key hash 之后取得所定位的桶。
      • 如果桶为空则直接返回 null 。
      • 否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。
      • 如果第一个不匹配,则判断它的下一个是红黑树还是链表。
      • 红黑树就按照树的查找方式返回值。
      • 不然就按照链表的方式遍历匹配返回值。
相同hashcode处理
  • 优化:减少碰撞。原理是如果两个不相等的对象返回不同的 hashcode 的话,那么碰撞的几率就会小些。这就意味着存链表结构减小,这样取值的话就不会频繁调用 equal 方法,从而提高 HashMap 的性能(扰动即 Hash 方法内部的算法实现,目的是让不同对象返回不同 hashcode)。使用不可变的、声明作 final 对象,并且采用合适的 equals() 和 hashCode() 方法,将会减少碰撞的发生。
  • 多线程HashMap死循环问题:因为如果两个线程都发现 HashMap 需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来。因为在resize过程中,移动到新的 bucket 位置的时候,HashMap 并不会将元素放在链表的尾部,而是放在头部,使用的是队头插入方法。这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了,这也是导致CPU飙升的原因,所以多线程的环境下不使用 HashMap,而是使用concurrentHashMap。

2.LinkedHashMap的介绍

  • 原理:LinkedHashMap的实现就是HashMap+LinkedList的实现方式,以HashMap维护数据结构,以LinkList的方式维护数据插入顺序。LinkedHashMap通过维护一个运行所有条目的双向链表,LinkedHashMap保证了元素的迭代顺序。迭代顺序可以是插入顺序或者是访问顺序。

3.TreeMap的介绍

  • 是一个有序的key-value集合,它是通过红黑树实现的。
  • 继承于AbstractMap,所以它是一个Map,即一个key-value集合。
  • 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。
  • 实现了Cloneable接口,意味着它能被克隆。
  • 实现了java.io.Serializable接口,意味着它支持序列化。
  • 基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
  • 的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。

4.HashTable与HashMap的区别

  • HashTable线程安全,HashMap线程不安全。
  • HashTable不可存储null值,HashMap可以存储null值。
  • HashMap去掉了HashTable的contains⽅方法,但是加上了 containsValue()和containsKey()⽅方法。

5. ConcurrentHashMap的详解

  • 结构:HashEntry +红黑树+CAS+Synchronized
  • 数据结构:Node(链表)、TreeNode(红黑树)、TreeBin(红黑树容器)
  • 原理:

put(k,v)流程:

  1. 如果没有初始化就调用initTable()。
  2. 如果hash没有冲突就直接CAS插入。
  3. 如果还在进行扩容就继续扩容(多线程进行)。
  4. 如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入。
  5. 插入最后一个元素如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环。
  6. 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容。
public V put(K key, V value) {
    return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode()); //两次hash,减少hash冲突,可以均匀分布
    int binCount = 0;
    for (Node[] tab = table;;) { //对这个table进行迭代
        Node f; int n, i, fh;
        //这里就是上面构造方法没有进行初始化,在这里进行判断,为null就调用initTable进行初始化,属于懒汉模式初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//如果i位置没有数据,就直接无锁插入
            if (casTabAt(tab, i, null,
                         new Node(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)//如果在进行扩容,则先进行扩容操作
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            //如果以上条件都不满足,那就要进行加锁操作,也就是存在hash冲突,锁住链表或者红黑树的头结点
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) { //表示该节点是链表结构
                        binCount = 1;
                        for (Node e = f;; ++binCount) {
                            K ek;
                            //这里涉及到相同的key进行put就会覆盖原先的value
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node pred = e;
                            if ((e = e.next) == null) {  //插入链表尾部
                                pred.next = new Node(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {//红黑树结构
                        Node p;
                        binCount = 2;
                        //红黑树结构旋转插入
                        if ((p = ((TreeBin)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) { //如果链表的长度大于8时就会进行红黑树的转换
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);//统计size,并且检查是否需要扩容
    return null;
}

get(k)流程:

  1. 计算hash值,定位到该table索引位置,如果是首节点符合就返回。
  2. 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回。
  3. 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null。
public V get(Object key) {
    Node[] tab; Node e, p; int n, eh; K ek;
    int h = spread(key.hashCode()); //计算两次hash
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {//读取首节点的Node元素
        if ((eh = e.hash) == h) { //如果该节点就是首节点就返回
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        //hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来
        //查找,查找到就返回
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) {//既不是首节点也不是ForwardingNode,那就往下遍历
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

你可能感兴趣的:(Java之Map家族)