HashMap全解析

来源:互联网 发布:淘宝网1 8米纯棉床罩 编辑:程序博客网 时间:2024/06/11 14:24

HashMap的使用场景

用于存储键值对的数据。但是存储数据不关心顺序,且需要较高性能的随机访问与存储数据速度。

HashMap数据结构

HashMap用一个指针数组(假设为table[])来做分散所有的key,
当一个key值数据被加入时,通过key的hash值与指针数组的长度计算获得数组下标i,然后就把key 值数据插到table[i]中,如果有两个不同的key被算在了同一个i,那么就叫冲突,又叫碰撞,这样会在table[i]上形成一个链表。

HashMap不足

我们知道,如果table[]的尺寸很小,比如只有2个,如果要放进10个keys的话,那么碰撞非常频繁,于是一个O(1)的查找算法,就变成了链表遍历,性能变成了O(n),这是Hash表的缺陷

所以,Hash表的尺寸和容量非常的重要。一般来说,Hash表这个容器当有数据要插入时,都会检查容量有没有超过设定的thredhold,如果超过,需要增大Hash表的尺寸,但是这样一来,整个Hash表里的元素都需要被重算一遍。这叫rehash或者resize,这个成本相当的大。

HashMap与ArrayList比较

  • HashMap与ArrayList都是基于数组实现的,都有各自的扩容机制
  • ArrayList数据存储与访问的时间复杂度是o(1),而HashMap的数据存储与访问的时间复杂度是o(n),所以在ArrayList性能略高于HashMap
  • HashMap多用于存储Key-Value键值对的数据,而ArrayList多用于随机存储与访问频繁的数据

HashMap与LinkedHashMap比较

  • HashMap与LinkedHashMap都是Map接口的实现类,都用于存储键值对形式的数据格式
  • LinkedHashMap是基于HashMap的,相对与HashMap它在对插入元素件建立器了双链表的数据结构,因此其遍历数据是有序的,也因此其性能略低于HashMap

HashMap与TreeMap比较

  • HashMap基于数组+链表的数据结构实现;TreeMap基于红黑树实现
  • HashMap存储数据是依赖hash值计算的索引以及指针碰撞情况,所以其在遍历时时无序的
  • TreeMap存储数据的顺序Comparable或者Comparator(自定义)比较器的实现
  • 当HashMap存储数据分布相对均匀的情况下,其查询与存储效率高于TreeMap(前提是不会频繁扩容)

HashMap存储Key值元素图解

  • 一般插入:
    这里写图片描述
    操作步骤:
    • 计算key=null的hash值(key值为null,hash值当0处理),并与table.length-1做&运算,得到下标i=0;
    • 由于下标0未存储元素,指针数组在下标0出存储插入的数据;
  • 指针碰撞插入:
    这里写图片描述
    操作步骤:
    • 计算key=null的hash值(key值为null,hash值当0处理),并与table.length-1做&运算,得到下标i=0;
    • 此时table[0]出存在元素,因此遍历下标元素引用的链表,得到尾部元素,并在尾部元素添加插入的数据;

HashMap查询Key值元素图解

  • HashMap查询指定Key值的元素与插入元素基本一致,步骤如下:
    • 计算key值hash值,并与table.size-1做&运算,计算得到指针数组下标i
    • 根据下标i,取得指针数组i处的单向链表,遍历链表查询获得与查询Key值相同的元素的Value值并返回
      这里写图片描述

HashMap并发访问问题

  • 指针数组中链表数据丢失
  • 新添加key值元素被覆盖,导致get依据该key值获取元素为null
  • 并发操作时,多个线程同时扩容并重建引用的时候,会出现环状链表,导致后续put操作或者get操作死循环,无法退出(原因是对共享变量next访问为加控制)
    • 经过笔者查看,JDK1.6与JDK1.7会存在循环,而JDK1.8将不会产生环状链表。

HashMap并发访问死循环分析

可以用以下代码触发(随机,不一定成功):
如果运行代码能程序能正常退出说明并发存储数据并未形成环状链表
另外,JDK8+版本是不会产生环状链表,原因是在resize()重建引用的时候,都将节点拷贝到局部变量处理并存储,从而在处理结束后只会存在数据被覆盖丢失的情况,不再存在环状链表以及原节点数据丢失的情况了。

package panlibin.study.collection;import java.util.HashMap;public class TestLock {    private HashMap map = new HashMap();    public boolean testLock() {        Thread[] putThreads = new Thread[10];        Thread[] getThreads = new Thread[10];        Runnable putRunnable = new Runnable() {            @Override            public void run() {                for (int i = 0; i < 50000; i++) {                    map.put(new Integer(i), i);                }            }        };        Thread putThread = null;        for (int i = 0; i < 10; i++) {            putThread = new Thread(putRunnable);            putThread.setName("put线程" + i);            putThreads[i] = putThread;        }        Runnable getRunnable = new Runnable() {            @Override            public void run() {                for (int i = 0; i < 50000; i++) {                    map.put(new Integer(i), i);                }            }        };        Thread getThread = null;        for (int i = 0; i < 10; i++) {            getThread = new Thread(getRunnable);            getThread.setName("get线程" + i);            getThreads[i] = getThread;        }        for (Thread thread : putThreads) {            thread.start();        }        int i = 0;        Thread[] endPutThreads = new Thread[10];        while (i < 10) {            for (int j = 0; j < 10; j++) {                Thread thread = putThreads[j];                if (thread == null) {                    continue;                }                // System.out.print("[" + thread.getName() + "]:" +                // thread.getState() + "\t");                if (thread.getState().equals(Thread.State.TERMINATED)) {                    i++;                    endPutThreads[j] = thread;                    putThreads[j] = null;                    if (i >= 10) {                        break;                    }                }            }            for (Thread thread : putThreads) {                if (thread == null) {                    continue;                }                System.out.print("[" + thread.getName() + "]:" + thread.getState() + "\t");            }            System.out.println();        }        System.out.println("=======================================================================================");        for (Thread thread : endPutThreads) {            System.out.print("[" + thread.getName() + "]:" + thread.getState() + "\t");        }        System.out.println();        System.out.println("=======================================================================================");        for (Thread thread : getThreads) {            thread.start();        }        Thread[] endGetTheads = new Thread[10];        i = 0;        while (i < 10) {            for (int j = 0; j < 10; j++) {                Thread thread = getThreads[j];                if (thread == null) {                    continue;                }                if (thread.getState().equals(Thread.State.TERMINATED)) {                    i++;                    endGetTheads[j] = thread;                    getThreads[j] = null;                    if (i >= 10) {                        break;                    }                }            }            for (Thread thread : getThreads) {                if (thread == null) {                    continue;                }                System.out.print("[" + thread.getName() + "]:" + thread.getState() + "\t");            }            System.out.println();        }        System.out.println("=======================================================================================");        for (Thread thread : endGetTheads) {            System.out.print("[" + thread.getName() + "]:" + thread.getState() + "\t");        }        System.out.println();        System.out.println("=======================================================================================");        return true;    }    public static void main(String[] args) {        int count = 100;        while (new TestLock().testLock()) {            if (count-- == 0) {                break;            }        }    }
参考(http://www.cnblogs.com/andy-zhou/p/5402984.html#_caption_1)此篇博客(最好观察代码来分析,jdk1.6与1.7查看方法transfer(Node<K,V> newTable);jdk1.8查看方法resize())
  • JDK1.6 与 1.7
    // JDK1.6    void transfer(Entry[] newTable) {        Entry[] src = table;        int newCapacity = newTable.length;        for (int j = 0; j < src.length; j++) {            Entry<K,V> e = src[j];            if (e != null) {                src[j] = null;                do {// 对链表重建                    /*                    前提:table[0]中链表的key的hash值都远远大于table.length,这样扩容后                        其下标计算将还是0;                    ---------------------------------------------------------                    table[0]   node0--->node1--->node2--->node3--->null                    ---------------------------------------------------------                    线程一操作start-拷贝table数组到局部变量操作                    操作前局部变量数据结构:                    取得操作链表首元素e1   node0--->node1--->node2--->node3--->null                                          ↑        ↑                                           e1      next1                    newTable[0]                    ---------------------------------------------------------                    线程一阻塞等待时间分片...                    线程二操作start-拷贝table数组到局部变量操作                    取得操作链表首元素e2   node0--->node1--->node2--->node3--->null                                          ↑        ↑                                           e2      next2                     newTable[0]                     ---------------------------------------------------------                    线程二阻塞等待时间分片...                    线程一操作start:do-while循环体执行到 Entry<K,V> next = e.next;(该句执行完成)                    操作链表:   node0--->node1--->node2--->node3--->null                                ↑        ↑                                  e2      next2                           newTable[0]   node0--->null                    ---------------------------------------------------------                    线程一阻塞等待时间分片...                    线程二操作start:do-while循环体内执行到 e = next(执行完成该句)                    操作链表:   node1--->node2--->node3--->null                                ↑      ↑                                  e2    next2                     newTable[0]   node0--->null                     ---------------------------------------------------------                     线程二阻塞等待时间分片...                     线程一操作start:do-while循环体执行到 e = next;(该句执行完成)                     操作链表:   node1--->node2--->node3--->null                                ↑        ↑                                  e1      next1                     newTable[0]   node0--->null                     线程一继续执行:由于e=node1不为null,继续循环,执行完e=next;                     操作链表:   node2--->node3--->null                                ↑        ↑                                  e1      next1                     newTable[0]   node1---->node0--->null                    ---------------------------------------------------------                    线程一阻塞等待时间分片...                    线程二操作start:do-while循环体继续执行,由于e2=node1,继续循环,                                   执行完成循环体                    此时线程二眼中的操作链表为:                                          node1--->node0--->null                                          ↑      ↑                                            e2    next2                    newTable[0]   node1--->node0--->null                    注意观察,如果线程二继续执行node0的next将会指向node1:                    操作数据结构:                                node1--->node0--->null                                                   ↑                                                         e2                        newTable[0]   node1--->node0                                    ↑        |                                    |---------|                    此时e=null;结束循环;                    就此***环形链表***形成                                           ---------------------------------------------------------                    注:将以上步骤以左右分布,纵轴为时间描述将会更清晰                    */                    Entry<K,V> next = e.next;                    int i = indexFor(e.hash, newCapacity);                    e.next = newTable[i];                    newTable[i] = e;                    e = next;                } while (e != null);            }        }    }    /** jdk1.7与此操作一致,无任何异同 **/
  • jdk1.8这里我们值做展示代码,具体就不分析了,jdk1.8多线程操作是不会产生-环状链表
        if (oldTab != null) {            for (int j = 0; j < oldCap; ++j) {                Node<K,V> e;                if ((e = oldTab[j]) != null) {                    oldTab[j] = null;                    if (e.next == null)                        newTab[e.hash & (newCap - 1)] = e;                    else if (e instanceof TreeNode)                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);                    else { // preserve order                        Node<K,V> loHead = null, loTail = null;// 局部存储,不操作原链表                        Node<K,V> hiHead = null, hiTail = null;// 局部存储,不操作原链表                        Node<K,V> next;                        do {                            next = e.next;                            if ((e.hash & oldCap) == 0) {                                if (loTail == null)                                    loHead = e;                                else                                    loTail.next = e;                                loTail = e;                            }                            else {                                if (hiTail == null)                                    hiHead = e;                                else                                    hiTail.next = e;                                hiTail = e;                            }                        } while ((e = next) != null);                        if (loTail != null) {                            loTail.next = null;                            newTab[j] = loHead;                        }                        if (hiTail != null) {                            hiTail.next = null;                            newTab[j + oldCap] = hiHead;                        }                    }                }            }        }

HashMap数据结构关键属性说明

  • 指针数组-table[]:HashMap用于分散存储数据的数组结构
  • 平衡因子-loadFactor:当插入元素个数(size)大于等于指针数组长度*平衡因子时,HashMap将扩容处理;也就是说该属性用于扩容计算临界判断
  • 插入元素个数-size:用于记录插入元素个数
  • 指针数组扩容临界值-threshold:指针数组长度*平衡因子,用户扩容判断依据
  • 数据结构变化次数-modCount:新增、删除、扩容,都会引起该值变化;在使用迭代器遍历是,如果该属性值变化,将会引发fast-fail,抛出异常

    下面的案例程序,基本能描述HashMap的大部分属性以及部分特性:

package panlibin.study.collection;import java.lang.reflect.Field;import java.util.Arrays;import java.util.HashMap;import java.util.Map;import java.util.Map.Entry;import org.junit.Test;public class MapTest {    @SuppressWarnings("unchecked")    @Test    public void hashMapTest1() {        HashMap map = new HashMap();        System.out.println("-----------------------------------初始化后关键属性值-----------------------------------");        Map.Entry[] entriesAf = (Map.Entry[]) getPrivateValue("table", map);        System.out.println("指针数组-table:" + Arrays.toString(entriesAf));        // System.out.println("数据存儲容器大小-table.length:"+entriesAf.length);        // 未初始化,会报错        System.out.println("平衡因子-loadFactor:" + getPrivateValue("loadFactor", map));        System.out.println("插入元素个数-size:" + getPrivateValue("size", map));        System.out.println("扩容临界值-threshold:" + getPrivateValue("threshold", map));        System.out.println("数据结构变化次数-modCount:" + getPrivateValue("modCount", map));        map.put(null, "null");        // Node<K,V>[] table 属性初始化——存储元素        System.out.println("-----------------------------------插入1个元素后关键属性值-----------------------------------");        Map.Entry[] entriesAfFirstInsert = (Map.Entry[]) getPrivateValue("table", map);        System.out.println("指针数组-table:" + Arrays.toString(entriesAfFirstInsert));        System.out.println("数据存儲容器大小-table.length:" + entriesAfFirstInsert.length);        System.out.println("平衡因子-loadFactor:" + getPrivateValue("loadFactor", map));        System.out.println("插入元素个数-size:" + getPrivateValue("size", map));        System.out.println("扩容临界值-threshold:" + getPrivateValue("threshold", map));        System.out.println("数据结构变化次数-modCount:" + getPrivateValue("modCount", map));        map.put(0, "元素0");        System.out.println("-----------------------------------插入2个元素后关键属性值-----------------------------------");        Map.Entry[] entriesAfSecInsert = (Map.Entry[]) getPrivateValue("table", map);        System.out.println("指针数组-table:" + Arrays.toString(entriesAfSecInsert));        System.out.println("数据存儲容器大小-table.length:" + entriesAfSecInsert.length);        System.out.println("平衡因子-loadFactor:" + getPrivateValue("loadFactor", map));        System.out.println("插入元素个数-size:" + getPrivateValue("size", map));        System.out.println("扩容临界值-threshold:" + getPrivateValue("threshold", map));        System.out.println("数据结构变化次数-modCount:" + getPrivateValue("modCount", map));        map.put(1, "元素1");        System.out.println("-----------------------------------插入3个元素后关键属性值-----------------------------------");        Map.Entry[] entriesAfThirdInsert = (Map.Entry[]) getPrivateValue("table", map);        System.out.println("指针数组-table:" + Arrays.toString(entriesAfThirdInsert));        System.out.println("数据存儲容器大小-table.length:" + entriesAfThirdInsert.length);        System.out.println("平衡因子-loadFactor:" + getPrivateValue("loadFactor", map));        System.out.println("插入元素个数-size:" + getPrivateValue("size", map));        System.out.println("扩容临界值-threshold:" + getPrivateValue("threshold", map));        System.out.println("数据结构变化次数-modCount:" + getPrivateValue("modCount", map));        map.put(2, "元素2");        map.put(3, "元素3");        map.put(4, "元素4");        map.put(5, "元素5");        map.put(6, "元素6");        map.put(7, "元素7");        map.put(8, "元素8");        map.put(9, "元素9");        map.put(16, "元素16");// 关注这个元素的变化        System.out.println("-----------------------------------插入12个元素后关键属性值-----------------------------------");        Map.Entry[] entriesAfTwelvethInsert = (Map.Entry[]) getPrivateValue("table", map);        System.out.println("指针数组-table:" + Arrays.toString(entriesAfTwelvethInsert));        System.out.println("数据存儲容器大小-table.length:" + entriesAfTwelvethInsert.length);        System.out.println("平衡因子-loadFactor:" + getPrivateValue("loadFactor", map));        System.out.println("插入元素个数-size:" + getPrivateValue("size", map));        System.out.println("扩容临界值-threshold:" + getPrivateValue("threshold", map));        System.out.println("数据结构变化次数-modCount:" + getPrivateValue("modCount", map));        map.put(10, "元素10");        System.out.println("-----------------------------------插入13个元素后关键属性值-----------------------------------");        Map.Entry[] entriesAfThirteenthInsert = (Map.Entry[]) getPrivateValue("table", map);        System.out.println("指针数组-table:" + Arrays.toString(entriesAfThirteenthInsert));        System.out.println("数据存儲容器大小-table.length:" + entriesAfThirteenthInsert.length);        System.out.println("平衡因子-loadFactor:" + getPrivateValue("loadFactor", map));        System.out.println("插入元素个数-size:" + getPrivateValue("size", map));        System.out.println("扩容临界值-threshold:" + getPrivateValue("threshold", map));        System.out.println("数据结构变化次数-modCount:" + getPrivateValue("modCount", map));        System.out.println("-----------------------------------指针碰撞后 元素0所处位置-----------------------------------");        Map.Entry entry = (Entry) getPrivateValue("next", entriesAfThirteenthInsert[0]);        System.out.println("元素0在table[0]位置上的链表尾节点:" + entry.toString());    }    // 获取实体指定属性的值    public Object getPrivateValue(String fieldName, Object obj) {        if (obj == null)            return null;        Class<? extends Map> cla = (Class<? extends Map>) obj.getClass();        Field field = null;        try {            field = cla.getDeclaredField(fieldName);            field.setAccessible(true);            return field.get(obj);        } catch (Exception e) {            e.printStackTrace();        } finally {            field.setAccessible(false);        }        return null;    }}//  -----------------------------------初始化后关键属性值-----------------------------------//  指针数组-table:null//  平衡因子-loadFactor:0.75//  插入元素个数-size:0//  扩容临界值-threshold:0//  数据结构变化次数-modCount:0//  -----------------------------------插入1个元素后关键属性值-----------------------------------//  指针数组-table:[null=null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]//  数据存儲容器大小-table.length:16//  平衡因子-loadFactor:0.75//  插入元素个数-size:1//  扩容临界值-threshold:12//  数据结构变化次数-modCount:1//  -----------------------------------插入2个元素后关键属性值-----------------------------------//  指针数组-table:[null=null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]//  数据存儲容器大小-table.length:16//  平衡因子-loadFactor:0.75//  插入元素个数-size:2//  扩容临界值-threshold:12//  数据结构变化次数-modCount:2//  -----------------------------------插入3个元素后关键属性值-----------------------------------//  指针数组-table:[null=null, 1=元素1, null, null, null, null, null, null, null, null, null, null, null, null, null, null]//  数据存儲容器大小-table.length:16//  平衡因子-loadFactor:0.75//  插入元素个数-size:3//  扩容临界值-threshold:12//  数据结构变化次数-modCount:3//  -----------------------------------插入12个元素后关键属性值-----------------------------------//  指针数组-table:[null=null, 1=元素1, 2=元素2, 3=元素3, 4=元素4, 5=元素5, 6=元素6, 7=元素7, 8=元素8, 9=元素9, null, null, null, null, null, null]//  数据存儲容器大小-table.length:16//  平衡因子-loadFactor:0.75//  插入元素个数-size:12//  扩容临界值-threshold:12//  数据结构变化次数-modCount:12//  -----------------------------------插入13个元素后关键属性值-----------------------------------//  指针数组-table:[null=null, 1=元素1, 2=元素2, 3=元素3, 4=元素4, 5=元素5, 6=元素6, 7=元素7, 8=元素8, 9=元素9, 10=元素10, null, null, null, null, null, 16=元素16, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]//  数据存儲容器大小-table.length:32//  平衡因子-loadFactor:0.75//  插入元素个数-size:13//  扩容临界值-threshold:24//  数据结构变化次数-modCount:13//  -----------------------------------指针碰撞后 元素0所处位置-----------------------------------//  元素0在table[0]位置上的链表尾节点:0=元素0
原创粉丝点击