HashMap和ConcurrentHashMap的区别
一、HashMap的底层原理
HashMap的底层是数组加链表的形式
-
当我们往HashMap中put元素时,利用key的hashCode重新计算出当前对象的元素在数组的下标
-
存储时,如果出现hash值相同的key,此时有两种情况
- 如果key相同,则覆盖原始值
- 如果key不同(出现冲突),则将当前的key-value放入链表中
-
获取时,直接找到hash值对应的下标,再进一步判断key是否相同,从而找到对应的值
-
在Java8之后,HashMap进行了优化,当链表长度大于8,且数组长度大于64时,链表将会转换为红黑树
-
在扩容resize()时,如果红黑树拆分的树的结点数小于等于6时,将退化成链表
二、ConcurrentHashMap的底层原理
ConcurrentHashMap的底层也使用了数组和链表(或1.8优化提供的红黑树)的组合来存储键值对,在Java8以前,它使用分段锁来保证线程安全。ConcurrentHashMap的结构示意图如下:
- 一个ConcurrentHashMap中有一个Segments数组,一个Segments中存储一个HashEntry数组,每个HashEntry是一个链表结构的元素。
-
那么,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的线程安全。
而在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冲突和加锁的频率也更低了,而在并发场景下的操作性能也提高了,数据量大的时候,查询性能也得到了进一步的提升。
-
总之,Java7中给Segment添加ReentrantLock重入锁来实现线程安全,在Java8中则是通过CAS或者synchronized来实现线程安全。
三、HashMap和ConcurrentHashMap的区别
了解了HashMap和ConcurrentHashMap的底层原理,接下来对他们的区别进行一个总结:
- 线程安全性:
HashMap
不是线程安全的,如果多个线程同时对其进行读写操作,可能会导致数据不一致或其他问题。ConcurrentHashMap
是线程安全的,它通过ReentrantLock重入锁(Java7)/CAS或者synchronized(Java8)来实现的。
- 性能:
- 在低并发情况下,
HashMap
的性能可能会优于ConcurrentHashMap
,因为ConcurrentHashMap
引入了额外的并发控制机制。 - 在高并发情况下,
ConcurrentHashMap
的性能通常会优于HashMap
,因为它能够更好地支持并发访问。
- 在低并发情况下,
总之,HashMap
和 ConcurrentHashMap
的底层都是基于数组和链表(或红黑树)的数据结构,但 ConcurrentHashMap
使用了额外的处理来实现线程安全和高效的并发访问。