HashMap和ConcurrentHashMap的区别

HashMap和ConcurrentHashMap的区别

一、HashMap的底层原理

HashMap的底层是数组加链表的形式

  1. 当我们往HashMap中put元素时,利用key的hashCode重新计算出当前对象的元素在数组的下标

  2. 存储时,如果出现hash值相同的key,此时有两种情况

    • 如果key相同,则覆盖原始值
    • 如果key不同(出现冲突),则将当前的key-value放入链表中
  3. 获取时,直接找到hash值对应的下标,再进一步判断key是否相同,从而找到对应的值

  4. 在Java8之后,HashMap进行了优化,当链表长度大于8,且数组长度大于64时,链表将会转换为红黑树

    image-20231121201648121

  5. 在扩容resize()时,如果红黑树拆分的树的结点数小于等于6时,将退化成链表

二、ConcurrentHashMap的底层原理

ConcurrentHashMap的底层也使用了数组和链表(或1.8优化提供的红黑树)的组合来存储键值对,在Java8以前,它使用分段锁来保证线程安全。ConcurrentHashMap的结构示意图如下:

image-20231121203701675

  1. 一个ConcurrentHashMap中有一个Segments数组,一个Segments中存储一个HashEntry数组,每个HashEntry是一个链表结构的元素。

image-20231121220400221

  1. 那么,ConcurrentHashMap是如何来保证线程安全的呢?

    Java7 中,ConcurrentHashMap 的 put 方法的源码如下:

    public V put(K key, V value) {
            Segment<K,V> s;
            if (value == null)
                throw new NullPointerException();
            int hash = hash(key);
            int j = (hash >>> segmentShift) & segmentMask;
            if ((s = (Segment<K,V>)UNSAFE.getObject
                 (segments, (j << SSHIFT) + SBASE)) == null)
                s = ensureSegment(j);
            return s.put(key, hash, value, false);
        }
    

    首先根据key的hashcode找到对应的segment段,执行segment段中的put方法

    final V put(K key, int hash, V value, boolean onlyIfAbsent) {
                HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
                V oldValue;
                try {
                    HashEntry<K,V>[] tab = table;
                    int index = (tab.length - 1) & hash;
                    HashEntry<K,V> first = entryAt(tab, index);
                    for (HashEntry<K,V> e = first;;) {
                        if (e != null) {
                            K k;
                            if ((k = e.key) == key ||
                                (e.hash == hash && key.equals(k))) {
                                oldValue = e.value;
                                if (!onlyIfAbsent) {
                                    e.value = value;
                                    ++modCount;
                                }
                                break;
                            }
                            e = e.next;
                        }
                        else {
                            if (node != null)
                                node.setNext(first);
                            else
                                node = new HashEntry<K,V>(hash, key, value, first);
                            int c = count + 1;
                            if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                                rehash(node);
                            else
                                setEntryAt(tab, index, node);
                            ++modCount;
                            count = c;
                            oldValue = null;
                            break;
                        }
                    }
                } finally {
                    unlock();
                }
                return oldValue;
            }
    

    在segment中的put方法,加锁lock(),再次hash确定存放的hashEntry数组中的位置,在链表中根据hash值和equals方法进行比较,如果相同就直接覆盖,如果不同就插入到链表中,最后把锁给释放。segment本身是基于ReentrantLock重入锁,来进行加锁和释放锁的操作,这样的话就能保证多线程同时访问ConcurrentHashMap时,同一时间只能有一个线程操作对应的节点,这样就保证了ConcurrentHashMap的线程安全。

    image-20231121231852422

    而在java8中,ConcurrentHashMap不再使用分段锁,它的put()方法源码如下

    public V put(K key, V value) {
            return putVal(key, value, false);
    }
    
    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;
                                }
                            }
                        }
                    }
                    if (binCount != 0) {
                        if (binCount >= TREEIFY_THRESHOLD)
                            treeifyBin(tab, i);
                        if (oldVal != null)
                            return oldVal;
                        break;
                    }
                }
            }
            addCount(1L, binCount);
            return null;
        }
    

    它主要是使用了CAS(比较和交换)加volatile或者是synchronized的方法来实现的,保证线程安全,我们可以从源码中看到,添加元素时,首先会判断容器是否为空,如果为空,就会使用volatile加CAS来初始化。如果容器不为空,就会根据存储的元素计算该位置是否为空,如果计算结果为空,就会利用CAS来设计该节点;如果计算结果不为空,就会使用synchronized加锁来进行实现,然后去遍历桶中的数据,并且替换或新增节点到桶中,最后判断是否需要转换为红黑树,这样就保证了并发访问的时候的线程安全了。

    如果把上面的执行用一句话来归纳的话,就相当于是ConcurrentHashMap通过对头结点加锁来保证线程安全,这样设计的好处是使得锁的粒度相比segment来说更小了,发生hash冲突和加锁的频率也更低了,而在并发场景下的操作性能也提高了,数据量大的时候,查询性能也得到了进一步的提升。

    image-20231121234207640

  2. 总之,Java7中给Segment添加ReentrantLock重入锁来实现线程安全,在Java8中则是通过CAS或者synchronized来实现线程安全。

三、HashMap和ConcurrentHashMap的区别

了解了HashMap和ConcurrentHashMap的底层原理,接下来对他们的区别进行一个总结:

  1. 线程安全性:
    • HashMap 不是线程安全的,如果多个线程同时对其进行读写操作,可能会导致数据不一致或其他问题。
    • ConcurrentHashMap 是线程安全的,它通过ReentrantLock重入锁(Java7)/CAS或者synchronized(Java8)来实现的。
  2. 性能:
    • 在低并发情况下,HashMap 的性能可能会优于 ConcurrentHashMap,因为 ConcurrentHashMap 引入了额外的并发控制机制。
    • 在高并发情况下,ConcurrentHashMap 的性能通常会优于 HashMap,因为它能够更好地支持并发访问。

总之,HashMapConcurrentHashMap 的底层都是基于数组和链表(或红黑树)的数据结构,但 ConcurrentHashMap 使用了额外的处理来实现线程安全和高效的并发访问。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: HashMapConcurrentHashMap都是Java中的Map实现,用于存储键值对数据。它们之间的主要区别在于线程安全性和并发性。 HashMap是非线程安全的,因此在多线程环境下使用可能会导致不一致的结果。而ConcurrentHashMap是线程安全的,它使用了锁分离技术和分段锁来保证并发性和线程安全性。 在使用上,HashMap适用于单线程环境或者多线程环境中不需要并发访问的场景。而ConcurrentHashMap则适用于多线程环境中需要高并发访问的场景。 另外,ConcurrentHashMap相对于HashMap还有一些额外的方法,比如putIfAbsent()和replace()等,用于更方便地进行并发操作。 总之,选择HashMap还是ConcurrentHashMap要根据实际情况进行考虑,根据需求选择适合的实现方式。 ### 回答2: HashMapConcurrentHashMap都是Java中的集合类,它们的区别主要体现在线程安全性和并发控制方面。 1. 线程安全性: HashMap是非线程安全的,多个线程同时访问和修改同一个HashMap实例会导致数据不一致或出现异常。 ConcurrentHashMap是线程安全的,它使用锁分段技术来保证多个线程可以同时访问不同的分段,从而提高并发性能。多个线程可以同时读取其中的数据,但对数据的修改操作会通过锁机制保证线程安全。 2. 并发控制: HashMap的并发控制是通过外部手段进行的,即通过在多线程环境下保证对HashMap的访问操作是互斥的,比如使用synchronized关键字或使用Lock接口。这样会导致多个线程在同一时刻只能有一个线程可以对HashMap进行操作,从而降低并发性能。 ConcurrentHashMap内部使用了分段锁技术,它将整个数据集分成多个小段,每个小段有一个独立的锁,不同的线程可以同时对不同的小段进行修改操作。这样多个线程可以并发的进行读取和修改操作,提高了并发性能。 总的来说,HashMap适用于单线程环境或者多线程环境但是不存在并发修改的场景,而ConcurrentHashMap适用于多线程并发修改的场景。但是需要注意的是,由于ConcurrentHashMap在并发控制方面做了额外的工作,因此在某些情况下会比HashMap的效率略低,所以在选择使用时需要根据具体的应用场景进行判断。 ### 回答3: HashMapConcurrentHashMap都是Java中的Map接口的实现类,它们之间有几个主要的区别: 1. 线程安全性:HashMap是非线程安全的,而ConcurrentHashMap是线程安全的。在多线程环境下,如果多个线程同时对HashMap进行操作,可能会导致数据不一致或者出现异常。而ConcurrentHashMap通过使用锁分段技术来保证高并发情况下线程安全。 2. 性能:在单线程环境下,HashMap的性能优于ConcurrentHashMap。因为ConcurrentHashMap为了保证线程安全性,会引入额外的开销,比如使用锁来保证操作的原子性。但是在高并发环境下,ConcurrentHashMap的性能优于HashMap,因为ConcurrentHashMap可以支持多个线程同时进行读操作,而不需要进行同步操作。 3. 数据一致性:HashMap的数据一致性是弱一致性的,即在多线程环境下无法保证数据的实时一致性。而ConcurrentHashMap使用一种叫做"读写分离"的技术,能够保证读操作之间的一致性,但是读操作和写操作之间仍然存在一定的时间间隔。 4. 并发度:ConcurrentHashMap支持更高的并发度。在ConcurrentHashMap中,它的数据结构是由多个Segment(分段锁)组成的,每个Segment中包含一个HashEntry数组,每个HashEntry是一个链表。不同的线程可以同时对不同的Segment进行读写操作,从而提高了并发度。 综上所述,HashMap适用于单线程环境下对数据的读写操作,而ConcurrentHashMap适用于高并发环境下对数据的读写操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值