HashMap、ConcurrentHashMap、Hashtable简单分析以及简单实现HashMap

HashMap分析:

1.根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度。

2.HashMap非线程安全(并发情况下put操作可能会引起死循环,因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形,Entry的next节点永远不为空,就会产生死循环获取Entry)。

Hashtable分析:

1.使用synchronized来保证线程安全,但在线程竞争激烈的情况下Hashtable效率非常低下。

2.一个线程访问Hashtable的同步方法,其他线程也访问hashtable的同步方法时候,会进入阻塞或者轮询状态。

ConcurrentHashMap分析:

1.采用了锁分段技术。

2.将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时候,其他段的数据也能被其他线程访问。

3.线程安全。

 

分析:

因为在并发编程中,使用HashMap可能导致程序死循环,而使用线程安全的Hashtable效率又非常低下,基于两个原因所以有了ConcurrentHashMap上场机会。

 

HashMap内部实现:

 

结构:数组 + 链表 + 红黑树(JDK1.8增加了红黑树部分)

 

部分Node源码:  

 /**

     * Basic hash bin node, used for most  entries.  (See below for

     * TreeNode subclass, and in LinkedHashMap  for its Entry subclass.)

     */

    static class Node<K,V> implements  Map.Entry<K,V> {

        final int hash;    //定位数组索引位置

        final K key;

        V value;

        Node<K,V> next;    //链表的下一个node

        Node(int hash, K key, V value, Node<K,V>  next) {

            this.hash = hash;

            this.key = key;

            this.value = value;

            this.next = next;

        }

        public final K getKey()        { return  key; }

        public final V getValue()      { return  value; }

        public final String toString() { return  key + "=" + value; }

        public final int hashCode() {

            return Objects.hashCode(key) ^  Objects.hashCode(value);

        }

        public final V setValue(V newValue) {

            V oldValue = value;

            value = newValue;

            return oldValue;

        }

        public final boolean equals(Object o) {

            if (o == this)

                return true;

            if (o instanceof Map.Entry) {

                Map.Entry<?,?> e =  (Map.Entry<?,?>)o;

                if (Objects.equals(key,  e.getKey()) &&

                    Objects.equals(value,  e.getValue()))

                    return true;

            }

            return false;

        }

    }

1.Node是HashMap的一个静态内部类,实现Map.Entry<K,V>接口,本质是一个映射(键值对)。

2.HashM安排使用哈希表来存储。哈希表为解决冲突,可以采用开放地址法(再散列法)、链地址法、再哈希法、建立公共溢出区。Java中的HashMap采用的链地址法。即数组加链表的结合。当数据hash后得到数组下标,把数据放在对应下标元素的链表上。

 

几个字段:

transient Node<K,V>[] table //哈希表 即由Node结点组成的一维数组

int threshold    //记录当前容量下,最适合存放多少键值对(容量*负载因子)

final float loadFactory    //负载因子 默认0.75

transient int modCount    //记录操作数

transient int size    //实际存在的键值对数量

1.Node[] table初始长度默认16,loadFactory为负载因子默认0.75,threshold是HashMap所能容纳的最大数据量的Node个数。threshold = 容量 * loadFactory = 16 * 0.75 = 12。超过这个数目就需resize扩容。负载因子是对空间和时间效率的一个平衡选择,除非特殊情况比如内存空间很多而对时间效率要求很高,可以降低负载因子的值。可以减少哈希冲突。threshold 总小于容量 size总不大于threshold。

2.size是HashMap中实际存在的键值对数量。

3.modCount字段主要用来记录HashMap内部结构发生变化的次数。(put键值对,若是覆盖则不属于结构变化)。

4.可能要出现拉链过长的情况,一旦拉链过长则会严重影响性能。所以JDK1.8对数据结构做了进一步的优化,引入红黑树。

5.当链表长度太长(默认超过8)时候,链表就转换为红黑树,利用红黑树快速增删改查的特点以提高HashMap性能,所以会用到红黑树的插入、删除、查找等算法。

 

功能实现:

get

public V get(Object key) {

    Node<K,V> e;

    return (e = getNode(hash(key), key)) == null ? null : e.value;

}



final Node<K,V> getNode(int hash, Object key) {

    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

    if ((tab = table) != null && (n = tab.length) > 0 &&

        (first = tab[(n - 1) & hash]) != null) {

        if (first.hash == hash && // always check first node

            ((k = first.key) == key || (key != null && key.equals(k))))

            return first;

        if ((e = first.next) != null) {

            if (first instanceof TreeNode)

                return ((TreeNode<K,V>)first).getTreeNode(hash, key);

            do {

                if (e.hash == hash &&

                    ((k = e.key) == key || (key != null && key.equals(k))))

                    return e;

            } while ((e = e.next) != null);

        }

    }

    return null;

}



static final int hash(Object key) {

    int h;

    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

可以看出定位数组索引位置的过程为 

(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)  &  table.length


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;

    if ((tab = table) == null || (n = tab.length) == 0) //判断table是否为null或空

        n = (tab = resize()).length;    //为空则扩容

    if ((p = tab[i = (n - 1) & hash]) == null) //i = (n - 1) & hash]得到数组索引位置

        tab[i] = newNode(hash, key, value, null); //tab[i]为空则直接新建节点添加

    else {    //tab[i]不为空

        Node<K,V> e; K k;

        if (p.hash == hash &&

            ((k = p.key) == key || (key != null && key.equals(k))))    //判断tab[i]首个元素是否和key一样

            e = p;    //一样则直接覆盖

        else if (p instanceof TreeNode)    //不一样则判断tab[i]是否是红黑树

            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);     //是则在树中插入键值对

        else {    //不是 则遍历tab[i]

            for (int binCount = 0; ; ++binCount) {

                if ((e = p.next) == null) {

                    p.next = newNode(hash, key, value, null);     //链表插入

                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //长度大于8

                        treeifyBin(tab, hash);    //把链表转红黑树 在红黑树中操作

                    break;

                }

                if (e.hash == hash &&    

                    ((k = e.key) == key || (key != null && key.equals(k))))    //key存在则直接覆盖

                    break;

                p = e;

            }

        }

        //put进已经存在的key 覆盖后 直接返回 即结构未发送变化 不记录(modeCount不++)

        if (e != null) { // existing mapping for key    

            V oldValue = e.value;

            if (!onlyIfAbsent || oldValue == null)

                e.value = value;

            afterNodeAccess(e);

            return oldValue;

        }

    }

    ++modCount;

    if (++size > threshold)    //超过容量 则扩容

        resize();

    afterNodeInsertion(evict);

    return null;

}

resize:

扩容,即重新计算容量。方法是用一个新的数组代替旧数组。

final Node<K,V>[] resize() {

    Node<K,V>[] oldTab = table;    //旧数组

    //旧数组的cap总容量和thr最佳容量

    int oldCap = (oldTab == null) ? 0 : oldTab.length;

    int oldThr = threshold;

    int newCap, newThr = 0;

    if (oldCap > 0) {

        //就数组cap超过最大cap(2^30)

        if (oldCap >= MAXIMUM_CAPACITY) {

            //修改为int最大值(2^31-1)返回 以后不再扩容

            threshold = Integer.MAX_VALUE;

            return oldTab;

        }

        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

                 oldCap >= DEFAULT_INITIAL_CAPACITY)

                        //cap thr扩大两倍 

            newThr = oldThr << 1; // double threshold

    }

    //oldCap为0,oldThr不为0,将newCap设为oldThr

    else if (oldThr > 0) // initial capacity was placed in threshold

        newCap = oldThr;

    else { //oldCap、oldThr两个都为0               // zero initial threshold signifies using defaults

        newCap = DEFAULT_INITIAL_CAPACITY;    //newCap默认值16

        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);    //newThr = 默认loadFactor * 默认cap

    }

    //判断newThr是否计算

    if (newThr == 0) {    //没有计算,用新容量和负载因子计算

        float ft = (float)newCap * loadFactor;

        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?

                  (int)ft : Integer.MAX_VALUE);

    }

       //计算完后更新当前threshold

    threshold = newThr;

        //生成新的哈希表 即一维数组

    @SuppressWarnings({"rawtypes","unchecked"})

        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

    //更新哈希表

    table = newTab;

    //将以前哈希表中的元素移动到新哈希表中合适的位置

    if (oldTab != null) {

        //遍历原本所有元素

        for (int j = 0; j < oldCap; ++j) {

            Node<K,V> e;

            if ((e = oldTab[j]) != null) {    //元素不为null 存起来

                oldTab[j] = null;    //旧表元素置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;    //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;

                    }

                }

            }

        }

    }

    return newTab;

}

原来:

新表:

 

可见,每当HashMap扩容的时候需要重新计算,并且用新数组代替旧数组,所以如果在使用中 知道HashMap需要存多少键值,最好在创建时指定容量,防止put时候进制扩容。

 

简单模拟一个HashMap(不扩容,不使用红黑树)

/**

* Map接口

* @param <K>

* @param <V>

*/

public interface Map<K,V> {

    V put(K k, V v);

    V get(K k);

    int size();

    interface  Entry<K,V>{

        K getKey();

        V getValue();

    }

}
/**

* describe:HashMap简单模拟

*

* @author gary

* @date 2019/01/20

*/

public class HashMap<K, V> implements Map<K, V> {

    private static int DEFAULT_INITIAL_CAPACITY = 16;

    private Entry<K, V>[] table = null;

    private int size = 0;



    public HashMap() {

        table = new Entry[DEFAULT_INITIAL_CAPACITY];

    }



    public int hash(K k) {

        int x = k.hashCode() % DEFAULT_INITIAL_CAPACITY;

        return x >= 0 ? x : -x;

    }



    @Override

    public V put(K k, V v) {

        size ++;

        int index = hash(k);

        Entry<K, V> entry = table[index];



        if (entry == null) {

            table[index] = newEntry(k, v, null);

        } else {

            table[index] = newEntry(k, v, entry);

        }



        return table[index].getValue();

    }



    public Entry<K, V> newEntry(K k, V v, Entry<K, V> next) {

        return new Entry<K, V>(k, v, next);

    }



    @Override

    public V get(K k) {

        int index = hash(k);



        if (table[index] == null) {

            return null;

        }



        return find(k, table[index]);

    }



    public V find(K k, Entry<K, V> entry) {

        if (k == entry.getKey() || k.equals(entry.getKey())) {

            return entry.getValue();

        } else {

            if (entry.next != null) {

                return find(k, entry.next);

            }

        }

        return null;

    }



    @Override

    public int size() {

        return size;

    }



    class Entry<K, V> implements Map.Entry<K, V> {

        K k;

        V v;

        Entry<K, V> next;



        public Entry(K k, V v, Entry<K, V> next) {

            this.k = k;

            this.v = v;

            this.next = next;

        }



        @Override

        public K getKey() {

            return k;

        }



        @Override

        public V getValue() {

            return v;

        }

    }

}

/**

* describe:测试MyHashMap

*

* @author gary

* @date 2019/01/20

*/

public class TestForMyHashMap {

    public static void main(String[] args) {

        Map<String, Integer> map = new HashMap<String, Integer>();

        long start = System.currentTimeMillis();

        for (int i = 0; i < 10000; i++) {

            map.put("gary" + i, 30);

        }

        for (int i = 0; i < 10000; i++) {

            map.get("gary" + i);

        }

        long end = System.currentTimeMillis();

        System.out.println("map大小:" + map.size() + "时间:" + (end -start));

    }

}

结果为:

map大小:10000时间:62

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值