理解HashMap与ConcurrentHashMap

数据结构

数组+链表+(红黑树jdk>=8)

源码原理分析

HashMap属性

	//初始容量为16;
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
	//最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
	//默认负载因子0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
	//链表转化为红黑树的阈值;
    static final int TREEIFY_THRESHOLD = 8;
	//红黑树退化为链表的阈值;
    static final int UNTREEIFY_THRESHOLD = 6;
    //链表转红黑树时,hash表的长度阈值,小于64,则优先扩容;
    static final int MIN_TREEIFY_CAPACITY = 64;

	//存储数据的Node数组,每个Node是一个链表;
 	transient Node<K,V>[] table;
    //Entry的集合,遍历hash表可能会用到;
    transient Set<Map.Entry<K,V>> entrySet;
	//哈希表的长度;
    transient int size;
    final float loadFactor;

HashMap内部类Node

真正存储key-value的结构;

 static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        //指向下一个结点。
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
  }

put()

put流程:

  1. 判断容器是否为空、如果为空,则进行扩容;
  2. 对key进行hash运算,再进行与运算
    • 判断数组中该位置是否为空,为空,则插入;
    • 判断数组该位置的首节点是否和key相等,则覆盖
    • 判断数组改为的首页点是否为树结点,则插入红黑树里
    • 以上都不成立,则
      A:遍历该位置的链表,如果存在key相同结点,则覆盖
      B:如果不存在相同的结点,则插入链表的尾部;
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {

    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //这里tab=table获得存储数据Node数组;判断是否为空,第一次添加元素则进行扩容;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // (n-1)&hash求出 在具体在数组中的下标,若该数组元素为空,则直接参加。    
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        //上面的p=tab[(n-1)&hash], p.hash=hash;
        //这里的p可能是一条链表的头结点,也可能是一颗红黑树的头结点
        //比较已经存在的数组元素的哈希值是否等于新元素的哈希值 
        //且 判断p.key是否等于新元素的key, 或者 key是否和k相同
        //相同则 将存在的p节点传给新节点e;
        //判断第一个结点的key是否和新元素的key相等》。。
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
        //如果p.hash不等于hash,且key也不相等、首元素也是TreeNode,
        //该Node元素链表的长度已经超过了8,节点已经变为了树节点。添加红黑树节点。
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //遍历这个Node链表;
            for (int binCount = 0; ; ++binCount) {
                //e=p.next:遍历链表
                if ((e = p.next) == null) {
                    //如果整条链表没有一个结点和新元素相同,则在链表尾部挂上一个新节点。
                    p.next = newNode(hash, key, value, null);
                    //如果链表的结点数大于8时,转化为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //如果链表中某一个结点的哈希值等于新元素的哈希值
                //且 key相同。
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                //这里跳到条下一个结点,顺序遍历。
                p = e;
            }
        }
        //如果链表中存在和新元素 key相同的结点,则e不为空。
        if (e != null) { // existing mapping for key
    	//获得旧的value值。
            V oldValue = e.value;
            //为空则复制。
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            //返回为非空。oldValue;
            return oldValue;
        }
    }
    //长度+1,判断是否到了扩容的阈值,到了则进行扩容
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    //返回空。
    return null;
}

HashMap的负载因子为什么0.75

loadFactor大于0,且不能是无穷大,默认情况下是0.75;而负载因子的设置很重要,hashMap通过initailCapaciry * loadFactor的乘积来决定当容量到达阈值后进行扩容。

  • 如果loadFactor的值太小,会导致hashMap扩容的频率变大且hashMap中的元素利用率低,
  • 如果loadFactor的太大,则会导致在hashMap的每个元素利用率增大,但是发生碰撞的概率也升高了。

HashMap与HashTable的区别

  • 前者线程不安全,后者线程安全;
  • 前者可以存储key为null的数据,后者不能;
  • 前者有containsvalue和containsKey方法,后者有contains方法方法
  • 前者继承Map,后者继承Directory

JDK1.7与1.8中,HashMap的区别

  • JDK1.7使用头插法,JDK1.8使用尾插法
  • JDK1.7存储数据的是Entry结点,JDK1.8存储的是Node结点。
  • JDK1.7创建对象就会初始化容器,JDK1.8在第一次put操作才初始化容器;
  • JDK1.7使用数组+链表,JDK1.8使用数组+链表+红黑树

JDK1.7扩容死锁分析

死锁问题核心在于下面代码,多线程扩容导致形成的链表环!

在这里插入图片描述

  1. 记录oldHash表中e.next;
  2. rehash计算出数组的位置(hash表中桶的位置)
  3. e要插入链表的头部, 所以要先将e.next指向new hash表中的第一个 元素
  4. 将e放入到new hash表的头部
  5. 转移e到下一个节点, 继续循环下去
单线程扩容

假设:hash算法就是简单的key与length(数组长度)求余。hash表长度为2,如果不扩 容, 那么元素key为3,5,7按照计算(key%table.length)的话都应该碰撞到table[1]上。
扩容:hash表长度会扩容为4重新hash,key=3 会落到table[3]上(3%4=3), 当前 e.next为key(7), 继续while循环重新hash,key=7 会落到table[3]上(7%4=3), 产生碰撞, 这里采用的是头插入法,所以key=7的Entry会排在key=3前面(这里可以具体看while语句 中代码)当前e.next为key(5), 继续while循环重新hash,key=5 会落到table[1]上 (5%4=3), 当前e.next为null, 跳出while循环,resize结束。
在这里插入图片描述

多线程扩容

假设这里有两个线程同 时执行了put()操作,并进入了transfer()环节
在这里插入图片描述
在这里插入图片描述
从上面的图我们可以看到,因为线程1的 e 指向了 key(3),而 next 指向了 key(7),在线程 2 rehash 后,就指向了线程2 rehash 后的链表。 然后线程1被唤醒了:

  1. 执行e.next = newTable[i],于是 key(3)的 next 指向了线程1的新 Hash 表,因 为新 Hash 表为空,所以e.next = null,
  2. 执行newTable[i] = e,所以线程1的新 Hash 表第一个元素指向了线程2新 Hash 表的 key(3)。好了,e 处理完毕。
  3. 执行e = next,将 e 指向 next,所以新的 e 是 key(7)

然后该执行 key(3)的 next 节点 key(7)了:

  1. 现在的 e 节点是 key(7),首先执行Entry<K,V> next = e.next,那么 next 就是 key(3)了
  2. 执行e.next = newTable[i],于是key(7) 的 next 就成了 key(3)
  3. 执行newTable[i] = e,那么线程1的新 Hash 表第一个元素变成了 key(7)
  4. 执行e = next,将 e 指向 next,所以新的 e 是 key(3)

此时状态为:
在这里插入图片描述
然后又该执行 key(7)的 next 节点 key(3)了:

  1. 现在的 e 节点是 key(3),首先执行Entry<K,V> next = e.next,那么 next 就是 null
  2. 执行e.next = newTable[i],于是key(3) 的 next 就成了 key(7)
  3. 执行newTable[i] = e,那么线程1的新 Hash 表第一个元素变成了 key(3)
  4. 执行e = next,将 e 指向 next,所以新的 e 是 key(7)

这时候的状态如图所示:
在这里插入图片描述
很明显,环形链表出现了

Jdk8-扩容

Java8 HashMap扩容跳过了Jdk7扩容的坑,对源码进行了优化,采用高低位拆分转移方 式,避免了链表环的产生。
扩容前:
在这里插入图片描述
扩容后:
在这里插入图片描述

ConcurrentHashMap

数据结构

与HashMap类似,但是内部在数据写入时,加了同步机制(分段锁)保证线程安全,读操作无锁操作;在扩容时,老数据的转移是并发执行的,扩容效率会更高。

JDK1.7并发安全控制

Java7 ConcurrentHashMap基于ReentrantLock实现分段锁
每个segment都继承了ReentrantLock,相当于每个segment都是一把锁,并发量等于segment的数量。
在这里插入图片描述
Java8中 ConcurrentHashMap基于分段锁+CAS保证线程安全,分段锁基于synchronized 关键字实现;
在这里插入图片描述

重要成员变量

  1. LOAD_FACTOR: 负载因子, 默认75%, 当table使用率达到75%时, 为减少table 的hash碰撞, tabel长度将扩容一倍。负载因子计算: 元素总个数%table.lengh。

  2. TREEIFY_THRESHOLD: 默认8, 当链表长度达到8时, 将结构转变为红黑树。

  3. UNTREEIFY_THRESHOLD: 默认6, 红黑树转变为链表的阈值

  4. MIN_TRANSFER_STRIDE: 默认16, table扩容时, 每个线程最少迁移table的槽位个数

  5. MOVED: 值为-1, 当Node.hash为MOVED时, 代表着table正在扩容

  6. TREEBIN, 置为-2, 代表此元素后接红黑树。

  7. nextTable: table迁移过程临时变量, 在迁移过程中将元素全部迁移到nextTable 上

  8. sizeCtl: 用来标志table初始化和扩容的,不同的取值代表着不同的含义:
    A: 0: table还没有被初始化
    B: -1: table正在初始化
    C:小于-1:,表明table正在扩容
    D:大于0: 初始化完成后, 代表table最大存放元素 的个数, 默认为0.75*n

  9. : table容量从n扩到2n时, 是从索引n->1的元素开始迁移, transferIndex代表当前已经迁移的元素下标

  10. ForwardingNode:扩容期间, 若table某个元素为null, 那么该元素设置为 ForwardingNode, 当下个线程向这个元素插入数据时, 检查hashcode=MOVED, 就 会帮着扩容

put(key,value)

final V putVal(K key, V value, boolean onlyIfAbsent) {
       //Key和value都不可以为空;
        if (key == null || value == null) throw new NullPointerException();
        //获取在数组中的位置;
        int hash = spread(key.hashCode());
        int binCount = 0;
        //自旋操作,可能会有多个线程同时进来。
        
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
           
            if (tab == null || (n = tab.length) == 0)
                 //初始化容器;
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //判断容器中的这个位置是否为空,如果为空,则使用CAS在这个位置插入数据
            //可能多个线程同时在往一个位置插入数据,使用CAS
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
            //判断容器是否正在扩容,如果在扩容,则帮助扩容
                tab = helpTransfer(tab, f);
            else {
            // f为容器某个位置的首结点。
                V oldVal = null;
                synchronized (f) {
                //加锁,同步;防止同时写入数据
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            //遍历容器插入位置所在的链表;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                     //如果key相同,则覆盖。
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                //下一个结点为空,则说明已经到了链表尾部了,在链表尾部插入数据。
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                        //判断首节点是否为红黑树结点,是则插入红黑树中。
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                    //判断链表长度是否>8,是则转化为红黑树;
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

get(key)

 public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        //获取在容器中的位置
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) 
            {
            if ((eh = e.hash) == h) {
            //首节点哈希值相等,再比较他们的key值。
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                //首节点哈希值相等,key也相等,找到了value值。
                    return e.val;
            }
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {
            //首节点的哈希值相等,Key不相等。遍历,链表,找打对应的值
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值