HashMap是线程不安全的,在多线程环境下对某个对象中HashMap类型的实例变量进行操作时,可能会产生各种不符合预期的问题。
当在多线程并发向HashMap中插入(put)数据时候,可能会出现数据丢失。
我们知道HashMap是数组+链表在极端情况下会出现树的结构。
我们假设当前HashMap中table的状态如下:

此时t1和t2同时执行put,假设t1执行put(“key2”, “value2”),t2执行put(“key3”, “value3”),并且key2和key3的hash值与图中的key1相同。
那么正常情况下,put完成后,table的状态应该是下图二者其一

或者

此时我们来看看异常情况,部分源码如下
- if ((e = p.next) == null) { // #1
- p.next = newNode(hash, key, value, null); // #2
- }
我们假设,有这两个线程都执行到了#1代码位置,然后都判断当前key1处没有数据,需要新插入一个节点到key1中,此时会发生数据覆盖现象
我们假设第一个线程插入成功

另一个线程在执行代码时候就会将上一个线程插入的数据覆盖

此时就发生了线程安全问题。
Jdk7中ConcurrentHashMap的实现原理基于分段锁机制。
如何实现呢?这就用到了ConcurrentHashMap中最关键的Segment。
ConcurrentHashMap中维护着一个Segment数组,每个Segment可以看做是一个HashMap。
而Segment本身继承了ReentrantLock,它本身就是一个锁。
在Segment中通过HashEntry数组来维护其内部的hash表。
思路如下
如果你是多线程来操作ConcurrentHashMap,如果你们操作的Segment不一样,就可以同时进行,如果操作的是同一个Segment就需要串行处理,只能等待锁释放。
所以,JDK7中,ConcurrentHashMap的整体结构可以描述为下图这样子。
这也就是前文所说的每个Segment可以看做是一个HashMap。

JDK8与JDK7的实现由较大的不同,JDK8中不在使用Segment的概念,他更像HashMap的实现方式。
关于HashMap的原理,参考另一篇文章 HashMap原理及内部存储结构
这个结构基本和HashMap类型,主要是通过volatile和CAS等操作保证线程安全。

其中put操作的共享变量的存储和读取全部通过volatile或CAS的方式,保证了线程安全。
对应源码如下
- else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
- if (casTabAt(tab, i, null,
- new Node
(hash, key, value, null))) - break; // no lock when adding to empty bin
- }
-
- static final
Node tabAt(Node[] tab, int i) { - return (Node
)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE); - }
-
- static final
boolean casTabAt(Node[] tab, int i, - Node
c, Node v) { - return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
- }
参考:
总结如下: