HashMap源码分析

本文对HashMap的源码进行分析,以及解析ConcurrentHashMap是如何保证并发情况下的线程安全。

HashMap是基于Map接口来实现的,并且允许空值和空键,而且HashMap不保证有序,特别强调的是元素的顺序会随着HashMap的扩展而变化,这一点我们在下面会有所强调。

HashMap的扩容因子默认为0.75f,设置扩容因子的目的就是当capacity * loadFactor 大于阈值时,HashMap就会执行resize()来将HashMap的大小扩大两倍。另外一个重要的概念就是在JDK1.8中,HashMap做了进一步的优化,当产生hash冲突的时候,以前都是通过链表解决,现在增加了一个 static final int TREEIFY_THRESHOLD = 8 ,当链表的长度达到8时,为了提高效率,链表会扩展成为红黑树。
这样在下面的get操作的时候算法效率就可以由O(n)提高到O(logn)。

get实现原理:

public V get(Object key ){
    Node<K,V> E;
    return (e=getNode(hash(key),key))== null ? null :e.value;
}

getNode的实现比较简单,实现步骤如下:
1、首先通过first = tab[(n - 1) & hash]找到第一个匹配的hash值得节点,如果没有匹配成功就返回null;
2、然后判断找到的第一个节点是否等于我们需要的查找的Key,如果是就直接返回,否则就代表产生了碰撞,转移到红黑树或者链表中去查找; 3、如果这个节点是红黑的子节点,转到红黑树中查找;
4、否则就在链表中查找,如果没有就返回null。

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 && ((key=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 && ((key = e.key ) == key || (key !=null && key.equals(k)))){
                            return e;
                        }
                    }while((e = e.next) != null );
                }
            }
            return null;
        }

put 实现原理:

public V put(K key,V value){
    return putVal(hash(key),key,value,false,true);
}

put 方法的实现相比较get 稍微复杂一些,主要分为以下几个步骤:
1、第一个if语句,如果tab为空就创建;
2、第二个if语句,通过计算hash值,如果没有发生碰撞,就直接创建一个节点;
3、重点就是分析else语句了。

在分析else语句前,建议大家先从整体看下源码中的两个条件分支,写的非常清晰,甚至和上面的get的操作思路有些相似,分为以下几步:

1、首先判断节点是否存在,如果存在直接给e赋值;
2、判断节点是否是红黑树的节点,如果是从红黑树中查找,并赋值给e;
3、否则从链表中查找,并赋值给e,注意循环中加了一个检查条件 if(bindCount >= TREEIFY_THRESHOLD -1) ,TREEIFY_THRESHOLD 默认为8,就是如果达到了这个阈值,就讲链表扩展为红黑树;
4、最后通过检查e来看是否已经存在相应的Key,如果存在就更新value值,并返回。
5、否则跳出else循环之后,通过修改 ++modCount 记录修改次数,并且判断现在的hashMap是否需要resize().

p.s: afterNodeAccess(e);和afterNodeInsertion(evict);是留给HashMap的子类LinkedHashMap去实现的。

/**
* Implements Map.put and related methods
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/

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)
    n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
else {
    Node<K,V> e;
    K k;
    if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
        e = p;
    elseif (p instanceof TreeNode)
        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    else {
        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
                treeifyBin(tab, hash);
            break;
        }
        if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
            break;
        p = e;
    }
}
        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;
}

hash的实现:

static final int hash(Object key) {
        int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

hash的实现虽然比较简单,就是高16位不变,低16位和高16位做了一个异或。但是还比较有技巧的,我们先来看一下原作者对这段代码的解释:

Computes key.hashCode() and spreads (XORs) higher bits of hash to lower. Because the table uses power-of-two masking, sets of hashes that vary only in bits above the current mask will always collide. (Among known examples are sets of Float keys holding consecutive whole numbers in small tables.) So we apply a transform that spreads the impact of higher bits downward. There is a tradeoff between speed, utility, and quality of bit-spreading. Because many common sets of hashes are already reasonably distributed (so don’t benefit from spreading), and because we use trees to handle large sets of collisions in bins, we just XOR some shifted bits in the cheapest possible way to reduce systematic lossage, as well as to incorporate impact of the highest bits that would otherwise never be used in index calculations because of table bounds.

为什么要做异或呢?而不是直接(n - 1) & hash,因为这样很容易发生碰撞,比如假设n等于16,那么散列的其实只有低四位。

resize的实现:

resize的实现其实也很巧妙,我们来具体看一看。当超过阀值threshold的时候,就会resize,因为hashmap的长度永远都是2的幂,而resize就是将长度扩展为原来的两倍,所以节点的位置可能会继续保持不变,也有可能会在原来的位置上移动两次幂。

比如我们需要从16位扩展到32位,会发生如下变化:对于hash1来说因为第5位是0,所以会保持原位置不变,但是对于hash2来说因为第五个位置是1,所以相当于在原位置的基础上加上oldCapacity(16)。

resize的实现思路上面我们已经分析出来了,源代码我就不贴了,代码大家具体到jdk下面去看吧。


ConcurrentHashMap

一般讲HashMap的时候都会同时涉及到ConcurrentHashMap,ConcurrentHashMap在jdk1.7和jdk1.8中的实现有很大的差异,在这里我们只是大致看一下具体的话以后有机会的话我再单独写一篇文章介绍。上面我们提到了HashMap是一个非线程安全的类,HashMap不允许通过Iterator遍历的同时通过HashMap修改,否则会抛出ConcurrentModificationException。

我们可以通过Collections.synchronizedMap(hashmap)来让HashMap变得线程安全,但是这样的话效率会不如直接使用ConcurrentHashMap,简单的说一下ConcurrentHashMap的实现原理,HashTable虽然也是可以保证并发,不过性能远远不如jdk 1.5后新出现的ConcurrentHashMap。

  • jdk1.7

jdk1.7中的ConcurrentHashMap的底层数据结构仍然是数组和链表。与HashMap不同的是,ConcurrentHashMap最外层不是一个大的数组,而是一个Segment的数组,Segment继承于ReenLock。每个Segment包含一个与HashMap数据结构差不多的链表数组。

对于写操作,并不要求同时获取所有Segment的锁,因为那样相当于锁住了整个Map。这里要进行两次Hash去定位数据的存储位置,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会通过继承ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock方法去获取锁,超过指定次数就挂起,等待唤醒。 同时由于其它Segment的锁并未被获取,因此理论上可支持concurrencyLevel(等于Segment的个数)个线程安全的并发读写。
对于读操作,获取Key所在的Segment时,需要保证可见性。具体实现上可以使用volatile关键字,也可使用锁。但使用锁开销太大,而使用volatile时每次写操作都会让所有CPU内缓存无效,也有一定开销。

对于计算map的size操作,为更好支持并发操作,ConcurrentHashMap会在不上锁的前提逐个Segment计算3次size,如果某相邻两次计算获取的所有Segment的更新次数(每个Segment都与HashMap一样通过modCount跟踪自己的修改次数,Segment每修改一次其modCount加一)相等,说明这两次计算过程中无更新操作,则这两次计算出的总size相等,可直接作为最终结果返回。如果这三次计算过程中Map有更新,则对所有Segment加锁重新计算Size。

  • jdk1.8

Java 7为实现并行访问,引入了Segment这一结构,实现了分段锁,理论上最大并发度与Segment个数相等。Java 8为进一步提高并发性,摒弃了分段锁的方案,而是直接使用一个大的数组。同时为了提高哈希碰撞下的寻址性能,Java 8在链表长度超过一定阈值(8)时将链表(时间复杂度为O(N))转换为红黑树(时间复杂度为O(log(N)))。

对于put操作,如果Key对应的数组元素为null,则通过CAS操作将其设置为当前值。如果Key对应的数组元素(也即链表表头或者树的根元素)不为null,则对该元素使用synchronized关键字申请锁,然后进行操作。如果该put操作使得当前链表长度超过一定阈值,则将该链表转换为树,从而提高寻址效率。

对于读操作,由于数组被volatile关键字修饰,因此不用担心数组的可见性问题。同时每个元素是一个Node实例(Java 7中每个元素是一个HashEntry),它的Key值和hash值都由final修饰,不可变更,无须关心它们被修改后的可见性问题。而其Value及对下一个元素的引用由volatile修饰,可见性也有保障。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;
}

put方法和remove方法都会通过addCount方法维护Map的size。size方法通过sumCount获取由addCount方法维护的Map的size。

  • 扩展

看完HashMap源码的朋友,建议可以阅读一下TreeMap的源码,TreeMap 和 TreeSet 是 Java Collection Framework 的两个重要成员,其中 TreeMap 是 Map 接口的常用实现类,而 TreeSet 是 Set 接口的常用实现类。虽然 TreeMap 和 TreeSet 实现的接口规范不同,但 TreeSet 底层是通过 TreeMap 来实现的(如同HashSet底层是是通过HashMap来实现的一样),因此二者的实现方式完全一样,而 TreeMap 的实现就是红黑树算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值