HashMap 及 ConcurrentHashMap原理详解

 

 

一.HashMap 的工作原理

 

1.hashing的概念

       Hashing(散列法)是一种将字符组成的字符串转换为固定长度的数值的方法,称为散列法,也叫哈希法。

2.HashMap 内部结构

       map 结构图:

 

        jdk1.8中HashMap 继承了map类,且内部类Node实现了Map类中的Entry接口:

 

      由以上结构图可以看出,HashMap是数组和链表结合组成的复合结构,数组被分为一个个桶(bucket),每个桶可存储一个或多个Entry对象,在HashMap Node实现了Entry接口方法,每个Node对象包含四部分,分别为hash(哈希值)、key(键)、value(值),next(指向下一个Entry),key的哈希值决定了Node对象存储在哪一个数组索引下;如果Node中key的哈希值相同,则以链表形式存储。如果链表大小超过树形转换的阈值(TREEIFY_THRESHOLD= 8),链表就会被改造为树形结构。

 

如下图,TREEIFY_THRESHOLD的值默认为8:

3.HashMap 工作原理

      HashMap 使用了 hashing 原理,我们通过 put() 方法存储数据而通过 get() 获取数据。HashMap 允许使用null值和null键。HashMap储存的是键值对,HashMap很快。HashMap的实现是非同步的。

  • 使用put()方法:

      HashMap 会先给键调用hashCode()方法,根据返回的hashCode找到bucket(桶,即对应的索引)位置来储存Node对象。HashMap是在bucket中储存键对象和值对象。

根据key计算hash值,设得到的索引为x:

(1)如果table[x]为空,直接插入;

(2)如果table[x]不为空,使用equals()方法检测到待插入元素key和首元素key相同,直接覆盖该首元素value;

  (3)   如果table[x]不为空,使用equals()方法检测到待插入元素key和首元素key不相同,则判断是红黑树(treeNode),红黑树,则直接在红黑树中插入键值对;

(4)如果table[x]不为空,使用equals()方法检测到待插入元素key和首元素key不相同,则判断是红黑树(treeNode),不是红黑树,则判断table[x]中的链表长度是否大 于8,如果大于8,则把链表转换为红黑树,在红黑树中执行插入操作;

(5)如果table[x]不为空,使用equals()方法检测到待插入元素key和首元素key不相同,则判断是红黑树(treeNode),不是红黑树,则判断table[x]中的链表长度是否大 于8,没有大于8,则直接在链表中插入,遍历过程中会使用equals()方法检测是否有相同的key,如果有直接覆盖value,没有即插入;

(6)插入之后,HashMap 会判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容,扩容将会创建原来HashMap大小的两倍的bucket数组。

 

  • 使用get()方法:

 (1) 使用get(key)时key,HashMap 会给key调用hashCode()方法,根据返回的hashCode找到bucket;

 (1) 通过equals()方法比较桶的内部元素是否与key相等,若都不相等,则没有找到。若找到相等的key,则取出相等key记录的value。

 

4.HashMap 的负载因子(load factor)

        由源码可以看到,当HashMap 的构造函数中没有指定负载因子的大小时,会默认为设置为0.75。这就说明当一个 map 填满了 75% 的 bucket 时候,将会扩充HashMap 大小的两倍的 bucket 数组,并将原来的对象放入新的 bucket 数组中。这个过程叫作 rehashing,因为它调用 hash 方法找到新的 bucket 位置。

 

二.为什么说 String, Interger 这样的 wrapper 类适合作为键

        例如 String 是final 修饰的,即是不可变的。不可变性是key的必要条件,因为存储和获取的时候都要使用hashCode()方法进行处理,如果键值是可变的,就不能保证在放入时和获取时返回相同的 hashcode ,那么就不能从 HashMap 中找到你想要的对象。

 

三.HashMap 与 HashTable 对比区别

        HashMap 是非 synchronized 的,性能更好,HashMap 可以接受为 null 的 key-value,而 Hashtable 是线程安全的,比 HashMap 要慢,不接受 null 的 key-value。

  以下是源码截图:

 

(1)HashTable :

        由此可见HashTable 是同步的,线程安全的,且不接受value为null的值。

  

 

(2)HashMap:

        由此可见HashMap 是非同步的,非线程安全的,可接受value为null的值。

 

四.ConcurrentHashMap

 

1.ConcurrentHashMap是什么:

        ConcurrentHashMap最外层不是一个大的数组,而是一个 Segment 的数组。每个 Segment 包含一个与 HashMap 数据结构差不多的链表数组。

2.为什么使用ConcurrentHashMap:

        在 Java 中,HashMap 是非线程安全的,在多线程下可能会导致 map 数据错乱。为保证线程安全,可使用以下三种方式:

1)使用Hashtable

2)使用Collections.synchronizedMap类

3)使用ConcurrentHashMap

  

        虽然Hashtable 是一个线程安全的类,但是Hashtable 几乎所有的添加、删除、查询方法都加了synchronized同步锁!多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞等待需要的锁被释放,在多线程场景中性能就会非常差,所以不推荐使用 Hashtable !

        而使用synchronizedMap的使用是类似的,如下synchronizedMap的源码可以看到,synchronizedMap的使用实际在实例化的时候传入map实例,然后在方法中加锁,可以看出,synchronizedMap也是全表锁,和Hashtable 的使用情况类似,虽然保证了线程安全,但是在多线程场景中性能就也会非常差,所以不推荐使用 synchronizedMap

        因此为了解决线程安全和多线程访问性能差的问题,引进了ConcurrentHashMap类。

       

3.ConcurrentHashMap结构及原理:

 

        在jdk1.7版本中,ConcurrentHashMap结构如下:

 

         在读写某个 Key 时,先取该 Key 的哈希值。并将哈希值的高 N 位对 Segment 个数取模从而得到该 Key 应该属于哪个Segment,接着如同操作 HashMap 一样操作这个 Segment。

       

        Segment 继承自 ReentrantLock,可以很方便的对每一个 Segmen 上锁。

 

        读操作:

        获取 Key 所在的 Segment 时,需要保证可见性。具体实现上可以使用volatile关键字,也可使用锁。但使用锁开销太大,而使用volatile时每次写操作都会让所有CPU内缓存无效,也有一定开销。ConcurrentHashMap 使用如下方法保证可见性,取得最新的Segment:

Segment<K,V> s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)

 

        获取 Segment 中的 HashEntry 时也使用了类似方法:

HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
  (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE)

 

         写操作:

       对于写操作,并不要求同时获取所有 Segment 的锁,只需要获取该要查找的key所在的Segment 的锁。获取成功后就可以像操作一个普通的 HashMap 一样操作该 Segment,这样可保证该 Segment 的安全性,又不需要获取全局锁,其他Segment若没有被获取锁,即可被其他线程正常操作。理论上可支持线程安全的并发读写的个数为Segment的个数。    

        获取锁时,并不直接使用 lock 来获取,因为该方法获取锁失败时会挂起。事实上,它使用了自旋锁,如果 tryLock 获取锁失败,说明锁被其它线程占用,此时通过循环再次以 tryLock 的方式申请锁。如果在循环过程中该 Key 所对应的链表头被修改,则重置 retry 次数。如果 retry 次数超过一定值,则使用 lock 方法申请锁。

这里使用自旋锁是因为自旋锁的效率比较高,但是它消耗 CPU 资源比较多,因此在自旋次数超过阈值时切换为互斥锁。

 

 

        在jdk1.8版本中,ConcurrentHashMap结构如下:

 

        JDK1.8 中HashMap  添加了红黑树, ConcurrentHashMap也做了相应的优化,在JDK1.8 中 ConcurrentHashMap 类取消了 Segment 分段锁,采用 CAS + synchronized 来保证并发安全,数据结构跟 jdk1.8 中 HashMap 结构类似,都是数组 + 链表(当链表长度大于 8 时,链表结构转为红黑二叉树)结构。

        ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发,相比 JDK1.7 的 ConcurrentHashMap 效率提高了非常多。

 

 

put操作中使用CAS + synchronized,源码如下:

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        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) {
                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 {
                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)))) {
                                    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;
                            }
                        }
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

 

4.ConcurrentHashMap总结:

1) HashMap 在多线程环境下操作不安全,于是在 java.util.concurrent 包下,java 为我们提供了 ConcurrentHashMap 类,该类保证在多线程下对 HashMap 操作安全;

2)在 JDK1.7 中,ConcurrentHashMap 采用了分段锁策略,先分成一个个Segment 数组, Segment 中又有一个 HashMap, 不同点是 Segment 继承自 ReentrantLock,在操作的时候给 Segment 赋予了一个对象锁,从而保证多线程环境下并发操作安全。

3)但是 JDK1.7 中,HashMap 容易因为冲突链表过长,造成查询效率低,所以在 JDK1.8 中,HashMap 引入了红黑树特性,当冲突链表长度大于 8 时,会将链表转化成红黑二叉树结构。因此 ConcurrentHashMap对应 也采用了与 HashMap 类似的存储结构,且ConcurrentHashMap 在JDK1.8 中没有采用分段锁的策略,而是在元素的节点上采用 CAS + synchronized 操作来保证并发的安全性。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值