HashMap、Hashtable和ConcurrentHashMap原理解析

一、HashMap

1、HashMap基本概念

  HashMap是基于哈希表的Map接口的非同步实现。HashMap存储的是key-value键值对,并允许key和value为null。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体,其继承了这两种数据结构的优点,实现了寻址容易,插入删除也比较容易。

  HashMap的结构示意图:


  注意:HashMap元素的保存顺序和插入顺序不同,特别是它不能保证插入元素的顺序恒久不变,因为在HashMap扩容后会重新计算每个元素的hashCode,并以此调整元素位置。

2、HashMap存储原理

  HashMap内部维护了一个存储数据的Entry数组,另外设计一个哈希函数,根据每一个元素的Key(关键字)的hashCode值(key.hashCode(),Object类中包含hashCode方法,具体的类中可能会重写hashCode()方法)再重新计算一个hash值,hash和数组长度减一按位与就可以得到对应的Entry数组的下标,数组存储的元素是一个Entry类。

例如, 第一个键值对A进来。通过计算其key的hashCode得到的index=0。记做:Entry[0] = A。 
第二个键值对B,通过计算其index也等于0, HashMap会将B.next =A,Entry[0] =B, 

第三个键值对 C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方事实上存取了A,B,C三个键值对,它们通过next这个属性链接在一起。我们可以将这个地方称为桶。 对于不同的元素,可能计算出了相同的函数值,这样就产生了“冲突”,这就需要解决冲突,“直接定址”与“解决冲突”是哈希表的两大特点。

3、HashMap工作原理

扫描二维码关注公众号,回复: 928837 查看本文章

  HashMap是基于哈希法的原理,使用put(key, value)将对象存储到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,根据返回的hashCode调用hash()方法(HashMap类的里面有hash()方法)得到hash值,其用于找到bucket(桶)位置来储存Entry对象。HashMap是在bucket中储存Entry对象,并不是仅仅只在bucket中存储值。

  下面分析一下put()和get()方法的源码:

先给出需要的hash()和indexFor()方法的源码:

 //求hash值的方法,重新计算hash值  
    static int hash(int h) {//h是key的hashCode值    
        h ^= (h >>> 20) ^ (h >>> 12);    
        return h ^ (h >>> 7) ^ (h >>> 4);    
    }    
   
    // 返回h在数组中的索引值,这里用&代替取模,旨在提升效率   
    // h & (length-1)保证返回值的小于length    
    static int indexFor(int h, int length) {    
        return h & (length-1);    
    }

put()方法源码:

// 将“key-value”添加到HashMap中    
    public V put(K key, V value) {    
        // 若“key为null”,则将该键值对添加到table[0]中。    
        if (key == null)    
            return putForNullKey(value);    
        // 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。    
        int hash = hash(key.hashCode());    
        int i = indexFor(hash, table.length);    
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {    
            Object k;    
            // 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!    
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {    
                V oldValue = e.value;    
                e.value = value;    
                e.recordAccess(this);    
                return oldValue;    
            }    
        }    
   
        // 若“该key”对应的键值对不存在,则将“key-value”添加到table中    
        modCount++;  
        //将key-value添加到table[i]处  
        addEntry(hash, key, value, i);    
        return null;    
    }    
   
    // putForNullKey()的作用是将“key为null”键值对添加到table[0]位置    
    private V putForNullKey(V value) {    
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {    
            if (e.key == null) {    
                V oldValue = e.value;    
                e.value = value;    
                e.recordAccess(this);    
                return oldValue;    
            }    
        }    
        // 如果没有存在key为null的键值对,则直接题阿见到table[0]处!    
        modCount++;    
        addEntry(0, null, value, 0);    
        return null;    
    }

put方法的过程: 

1)、获取key;
2)、根据key的hashCode值,通过hash函数计算得到hash值;对应的代码:int hash = hash(key.hashCode());(再哈希法,为了减少哈希冲突)
3)、通过indexFor()方法,得到桶号(Entry数组的下标)index,方法是将hash值与Entry数组长度减一按位与:hash & (table.length-1)。
4)、 存放key和value在桶内。table[index]=Entry对象;

综述:当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode值用于找到桶号来储存Entry对象。

get方法源码:

// 获取key对应的value    
    public V get(Object key) {    
        if (key == null)    
            return getForNullKey();    
        // 获取key的hash值    
        int hash = hash(key.hashCode());    
        // 在“该hash值对应的链表”上查找“键值等于key”的元素    
        for (Entry<K,V> e = table[indexFor(hash, table.length)];    
             e != null;    
             e = e.next) {    
            Object k;    
            //判断key是否相同  
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))    
                return e.value;    
        }  
        //没找到则返回null  
        return null;    
    }    
   
    // 获取“key为null”的元素的值    
    // HashMap将“key为null”的元素存储在table[0]位置,但不一定是该链表的第一个位置!    
    private V getForNullKey() {    
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {    
            if (e.key == null)    
                return e.value;    
        }    
        return null;    
    }

get方法的过程:

1)、获取key ;
2)、根据key的hashCode值,通过hash函数计算得到hash值;对应的代码:int hash = hash(key.hashCode());(再哈希法)
3)、通过indexFor()方法,得到桶号(Entry数组的下标)index,方法是将hash值与Entry数组长度减一按位与:                          hash & (table.length-1)。(2、3两步与put方法相同)

4)、比较桶的内部元素是否与key相等,若都不相等,则没有找到。比较的时候遍历链表,依次与key比较。

5)、取出相等的记录的value。

综述:当我们给get()方法传递键时,我们先对键调用hashCode()方法,返回的hashCode值用于找到桶号,比较桶内元素的key是否与key相同,若相同,则取出对应的value;若都不相同,则没有找到。

4、HashMap的碰撞与解决方法

  当两个不同的对象的hashCode相同时,它们的bucket位置相同,“碰撞”会发生。因为HashMap使用LinkedList存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在LinkedList中。

  当我们调用get()方法,HashMap会使用键对象的hashCode找到bucket位置,然后遍历LinkedList直到找到值对象。找到bucket位置之后,会调用key.equals()方法去找到LinkedList中正确的节点。所以碰撞发生的话,会增加比较的次数。

5、HashMap的初始容量及扩容

  HashMap内Entry数组的初始长度默认是16,当Entry数组的利用率超过一个阈值(加载因子)时,HashMap会进行扩容。

  默认的加载因子大小为0.75,也就是说,当一个HashMap填满了75%的bucket时候,将会创建原来HashMap大小的两倍的bucket数组(HashMap大小永远都是2的整数幂),来重新调整HashMap的大小,并将原来的对象放入新的bucket数组中(会重新计算每个Entry对象的桶号)。

  重新调整HashMap大小存在什么问题吗?在多线程的情况下,可能产生条件竞争。因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在LinkedList中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在LinkedList的尾部,而是放在头部,这是为了避免尾部遍历。如果条件竞争发生了,那么就死循环了(链表会形成一个环)。

二、Hashtable

1、基本概念

  Hashtable是基于哈希表的Map接口的同步实现,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会进行扩容。Hashtable使用synchronized来保证线程安全,但在线程竞争激烈的情况下,Hashtable的效率非常低下。因为当一个线程访问Hashtable的同步方法时,其他线程访问Hashtable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。

2、Hashtable与HashMap的联系与区别

  1、二者的存储结构和解决冲突的方法都是相同的。
  2、Hashtable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。

  3、Hashtable中key和value都不允许为null,而HashMap中key和value都允许为null(key只能有一个为null,而value则可以有多个为null)。但是如果在Hashtable中有类似put(null,null)的操作,编译同样可以通过,因为key和value都是Object类型,但运行时会抛出NullPointerException异常。

4、Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。

5、Hashtable计算hash值,直接用key的hashCode(),而HashMap重新计算了key的hash值,Hashtable在求hash值对应的位置索引时,用取模运算;而HashMap在求位置索引时,hash值与Entry数组长度减一做按位与,即index= hash & (length-1)。

三、ConcurrentHashMap



更新中。。。

猜你喜欢

转载自blog.csdn.net/karute/article/details/80282929