搞定HashMap线程不安全问题-----ConcurrentHashMap源码解析
前言
HashMap是线程不安全的集合,如果要保证线程安全该怎么做呢?
首先,HashMap为什么会线程不安全?
-
jdk1.7中,在多线程环境下,(头插法)扩容时会造成环形链或数据丢失。
-
jdk1.8中,在多线程环境下,PUT方法会发生数据覆盖的情况。
如何保证线程安全?
//替代HashMap的方式
public static void main(String[] args) {
/**方式0**/
HashMap<Object,Object> map = new HashMap<>();
/**方式1**/
Collections.synchronizedMap(new HashMap<>());
/**方式2**/
Hashtable hashtable = new Hashtable();
/**方式3**/
ConcurrentHashMap<Object,Object> concurrentHashMap = new ConcurrentHashMap<Object,Object>(32,0.75f,32);
}
-
Collections.synchronizedMap(Map)创建线程安全的map集合
看源码可知,synchronizedMap把创建的对象map赋给要同步的对象mutex,之后每次操作map都会在方法上加上锁。
-
HashTable
HashTable中对数据操作的大部分方法都会上锁,与方式1原理相似。如图中HashTable源码所示。
缺点就是效率低,有些时候没必要加锁,却仍要线程等待。
hashtable的put方法:
-
ConcurrentHashMap
HashMap是由Entry<K, V>[]数组(jdk1.7), Node<K, V>[]数组(jdk1.8)组成,
而ConcurrentHashMap是由 Segment[HashEntry[]] (jdk1.7),Node<K, V>[]数组(jdk1.8)组成。
在jdk1.7中ConcurrentHashMap的底层数据结构是数组(Segment)加链表(HashEntry),可以看到内部类Segment的结构
/** Segment类继承于ReentrantLock类,所以Segment本质上是一个可重入的互斥锁 */ static final class Segment<K,V> extends ReentrantLock implements Serializable { private static final long serialVersionUID = 2249069246763182397L; //存放数据的节点,与HashMap不同,用volatile修饰value和next节点 transient volatile HashEntry<K,V>[] table; transient int count; transient int modCount; transient int threshold; final float loadFactor; }
在来看jdk1.7中ConcurrentHashMap的put方法
/** 1 先定位到Segment,然后进行put操作 */ public V put(K key, V value) { Segment<K,V> s; //注意不能put空value if (value == null) throw new NullPointerException(); int hash = hash(key); int j = (hash >>> segmentShift) & segmentMask; // 如果找不到该Segment,则新建一个。 if ((s = (Segment<K,V>)UNSAFE.getObject (segments, (j << SSHIFT)<