hashmap 源码分析_1.8

一、属性

(n - 1) & hash == hash%n

/**
 1. 容量(capacity): HashMap中数组的长度
     容量范围:必须是2的幂,最大容量:2的30次方
 */
 
 //默认容量 = 16 = 1<<4 
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
  
//最大容量 =  2的30次方(若传入的容量过大,将被最大值替换)
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 2. 加载因子(Load factor):HashMap在其容量自动增加前可达到多满的一种尺度
    a. 加载因子越大、填满的元素越多 = 空间利用率高、但冲突的机会加大、查找效率变低(因为链表变长了)
    b. 加载因子越小、填满的元素越少 = 空间利用率小、冲突的机会减小、查找效率高(链表不长)
 */
 
 // 加载因子
  final float loadFactor;
  
// 默认加载因子的值 = 0.75
  static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 3. 扩容阈值(threshold):当哈希表 容量(table 的 size) ≥ 扩容阈值时,就会扩容哈希表(即扩充HashMap的容量) 
    a. 扩容 = 对哈希表进行resize操作(即重建内部数据结构),将hashmap 的容量增加一倍
    b. 扩容阈值 = 容量(table.size) x 加载因子(loadFactor)
 */
//扩容阈值: 当实际大小(容量*填充比 = table.size * loadFactor ) > threshold,会进行扩容    
  int threshold;
  
  
//4.当add一个元素到某个位桶,其链表长度达到8时将链表转换为红黑树  
      static final int TREEIFY_THRESHOLD = 8;  
      static final int UNTREEIFY_THRESHOLD = 6;  
      static final int MIN_TREEIFY_CAPACITY = 64;   

//5.位桶数组,默认长度16,终极大 boss
transient Node<k,v>[] table;       

//6.其他

//HashMap的大小,即 HashMap中存储的键值对的数量
transient int size;
//被修改的次数fast-fail机制  
transient int modCount;

二、构造函数

//构造函数1
public HashMap(int initialCapacity, float loadFactor) {
    //指定的初始容量非负
    if (initialCapacity < 0)
        throw new IllegalArgumentException(Illegal initial capacity:  +
                                           initialCapacity);
    //如果指定的初始容量大于最大容量,置为最大容量
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //填充比为正
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException(Illegal load factor:  +
                                           loadFactor);
                                           
    //默认 0.75                                       
    this.loadFactor = loadFactor;
    
    //tableSizeFor 负责根据 initialCapacity的值,找到initialCapacity最近的一个幂值
    //默认16
    this.threshold = tableSizeFor(initialCapacity);//新的扩容临界值
}

//构造函数2
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

//构造函数3
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

//构造函数4用m的元素初始化散列映射
public HashMap(Map<!--? extends K, ? extends V--> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

三、HashMap的存取机制

1.HashMap如何getValue值,看源码

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

    /**
     * Implements Map.get and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab;
        Node<K,V> first, e; 
        int n; 
        K k;
        
        //判断中做赋值操作也是醉了
        //1.判断  位桶数组 table 是否为空;table 的长度大于0;桶位的第一个元素不是空
        //2.把 table 赋值给 tab; 把桶中的 第一个元素赋值给 first
        //特别的.『(n - 1) & hash』表示 hash 对 table 的长度取余
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //如果 hash 值吻合 且  key 也对  那么返回 位桶中的第一个元素
            if (first.hash == hash &&  ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //如果位桶中的第一个元素不是想找的元素,那么走下面逻辑    
            if ((e = first.next) != null) {
                //如果第一个元素属于 TreeNode(可能是判断是否红黑树) 
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                //循环 当前位桶 中的链表元素,直到找到和查询的 key 一致的元素
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

get(key)方法时获取key的hash值,计算hash&(n-1)得到在链表数组中的位置first=tab[hash&(n-1)],先判断first的key是否与参数key相等,不等就遍历后面的链表找到相同的key值返回对应的Value值即可

2.HashMap如何put(key,value);看源码

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;
        
        //1.如果 table 是 null  那么先进行扩容处理(put 第一个元素的时候)
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //2.如果当前位桶 是个空的那么 进行 newNode 的操作 
            //即使不是空的,找到当前 key 的 hash 对应的位桶  p 指向当前位桶的首节点(重要:对下面的 else 有影响)
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
        //3.当 hash 有冲突(即 当前位桶已经有了元素:(n - 1) & hash是相等的<两种情况:1.key 相等  2.key 不相等,和 n取模相等>)
            Node<K,V> e; K k;
            //3.1如果首节点 的 key 和  需要添加的 key 是同一个 key,那么把首节点赋值给 e(注意是地址赋值,只要 e 有变化 p、tab、table 都会有变化),在 ★ ★ ★ ★ ★ ★处,把新值替换进去,老值返回
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))){
                e = p;
            }else if (p instanceof TreeNode){
             //3.2 红黑树操作  
             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            }else {
            //3.3 重点:如果hash 对 n 取模相同, key 又不同(需要放在同一个位桶,但是不是同一个 key)
                //此循环的意思是找到当前位桶的最后一个 node,把新 node 挂在最后一个 node 上面
                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 已经不在指向 table 当前位桶的首元素了。    
                    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;
    }
  1. 判断键值对数组tab[]是否为空或为null,否则以默认大小resize();
  2. 根据键值key计算hash值得到插入的数组索引i,如果tab[i]==null,直接新建节点添加,否则转入3
  3. 判断当前数组中处理hash冲突的方式为链表还是红黑树(check第一个节点类型即可),分别处理
  4. 如果是链表,那么把新元素放到队尾

四、HasMap的扩容机制resize();

构造hash表时,如果不指明初始大小,默认大小为16(即Node数组大小16),如果Node[]数组中的元素达到(填充比*Node.length)重新调整HashMap大小 变为原来2倍大小,扩容很耗时

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
    //记录扩容    
        //table 位桶 数组的大小(位桶的容量)
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //扩容阈值
        int oldThr = threshold;
        //新的位桶数组容量,扩容阈值
        int newCap, newThr = 0;
        //如果旧的 table 位桶数组容量 > 0
        if (oldCap > 0) {
            //如果旧的 table 位桶数组容量 >= 最大容量:1 << 30
            if (oldCap >= MAXIMUM_CAPACITY) {
                //那么把扩容阈值设置为 最大(Integer.MAX_VALUE > MAXIMUM_CAPACITY):这就意味着 当table 位桶的容量达到最大后,就不再进行扩容了
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //1.把旧的 位桶 table 容量增加一倍
            //2.如果新扩容出来的 容量 < 最大容量,且原容量也不是最小容量,那么就把扩容阈值增加一倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY){
                         newThr = oldThr << 1; // double threshold
                     }
                
        }
        
        //下面  else if 和 else  是初始化的两个操作
        //(new HashMap 时 使用有参构造器<指定容量>)如果 原容量是0,且 扩容阈值 > 0,那么把构造器中拿到的初始容量赋值给newCap 
        else if (oldThr > 0){// initial capacity was placed in threshold
            //如果指定容量 new hashmap,存容量值的是threshold(扩容阈值),所以在这里把指定的容量值还给newCap,在重新构造 node 数组的时候使用
            newCap = oldThr;
        }
        //(new Hashmap 时 是无参构造器<不指定容量> )当原容量 和 扩容阈值都是0  证明new hashmap 时是无参构造器,那么容量就是16,扩容阈值 是  16*0.75
        else { // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //如果是  有参构造器的初始化。扩容阈值newThr = 容量 * loadFactor
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //最终确定的 扩容因子 赋值给  threshold
        threshold = newThr;
        //构建新的Node 数组,并时 位桶 table 指向该数组
        @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;
                //1.把当前位桶的单链表交给 e,然后把当前的位桶清空
                //2.当前位桶如果 是空,那么啥事也不做
                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
                        //看此逻辑重点理解 (e.hash & oldCap)
                        //比如oldCap=8,hash是3,11,19,27时,(e.hash & oldCap)的结果是0,8,0,8,这样3,19组成新的链表,index为3;而11,27组成新的链表,新分配的index为3+8;
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //通过e.hash & oldCap 把原 位桶中的链表分成  两个链表(e.hash & oldCap) == 0 的放在原 位桶中,else 组成的链表放到  原位桶的index+oldCap中)
                            //while 循环的过程,其实是两个单链表不断的 链元素的过程
                            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) {
                            //(e.hash & oldCap) == 0 的放在原位桶中
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            //else 组成的链表放到  原位桶的index+oldCap中
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

jdk 1.7 resize 重新分配元素使用的是(e.hash & oldCap-1) hask 值 对 oldCap取余来获取坐标
jdk 1.8 通过(e.hash & oldCap) 查看是否为0 来把原链表分裂成两个链表。为0的在原位桶不变,不为0的 原位桶 index + oldCap

五、JDK1.8使用红黑树的改进

在jdk7中,HashMap处理“碰撞”的时候,都是采用链表来存储,当碰撞的结点很多时,查询时间是O(n)。
在jdk8中,HashMap处理“碰撞”增加了红黑树这种数据结构,当碰撞结点较少时,采用链表存储,当较大时(>=8个),采用红黑树。
当当前位桶元素>=8时 转为红黑树,当元素<= 6 时从红黑树转为单链表

六、HashMap线程不安全的体现

jdk1.7

1.resize 时的 transfer函数

①②③

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i]; //①
                newTable[i] = e;//②
                e = next;//③
            }
        }
    }

如果两个线程同时去 resize 的时候,比如 A 线程 resize 完毕,B 线程再去根据 A 线程 resize 完毕后的结构去 resize,因为 resize 方法中使用了头插法去排列元素,所有就有可能会造成链表元素形成环形链表,在 get 这个链表元素的时候形成死循环。也有可能不形成环形链表,但是会丢元素。

2.put 元素时候也会因为不是同步的方法而可能丢失元素

jdk1.8

在jdk1.8中对HashMap进行了优化,在发生hash碰撞,不再采用头插法方式,而是直接插入链表尾部,因此不会出现环形链表的情况,但是在多线程的情况下仍然不安全,这里我们看jdk1.8中HashMap的put操作源码:

 1  final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 2                    boolean evict) {
 3         Node<K,V>[] tab; Node<K,V> p; int n, i;
 4         if ((tab = table) == null || (n = tab.length) == 0)
 5             n = (tab = resize()).length;
 6         if ((p = tab[i = (n - 1) & hash]) == null) // 如果没有hash碰撞则直接插入元素
 7             tab[i] = newNode(hash, key, value, null);
 8         else {
                ....... 省略下面代码

这是jdk1.8中HashMap中put操作的主函数, 注意第6行代码,如果没有hash碰撞则会直接插入元素。如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,所以这线程A、B都会进入第6行代码中。假设一种情况,线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全。

参考:
https://blog.csdn.net/bnmb888/article/details/77164485
https://www.cnblogs.com/little-fly/p/7344285.html
https://www.cnblogs.com/developer_chan/p/10450908.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值