浅析HashMap与ConcurrentHashMap的线程安全性

本文要解决的问题: 最近无心中发现有不少对Map尤为是HashMap的线程安全性的话题讨论,在个人理解中,对HashMap的理解中也就知道它是线程不安全的,以及HashMap的底层算法采用了链地址法来解决哈希冲突的知识,可是对其线程安全性的认知有限,故写这篇博客的目的就是让和我同样对这块内容不熟悉的小伙伴有一个对HashMap更深的认知。node

哈希表 在数据结构中有一种称为哈希表的数据结构,它其实是数组的推广。若是有一个数组,要最有效的查找某个元素的位置,若是存储空间足够大,那么能够对每一个元素和内存中的某个地址对应起来,而后把每一个元素的地址用一个数组(这个数组也称为哈希表)存储起来,而后经过数组下标就能够直接找到某个元素了。这种方法术语叫作直接寻址法。这种方法的关键是要把每一个元素和某个地址对应起来,因此若是当一组数据的取值范围很大的时候,而地址的空间又有限,那么必然会有多个映射到同一个地址,术语上称为哈希冲突,这时映射到同一个地址的元素称为同义词。毕竟,存储空间有限,因此冲突是不可避免的,可是能够尽可能作到减小冲突。目前有两种比较有效的方法来解决哈希冲突:算法

链地址法 开放地址法 这里简要说明一下开放地址法,顾名思义,就是哈希表中的每一个位置要么存储了一个元素要么为NULL。当数据比较多的时候,查找一个元素挺费事的,可是可使用探测的方法进行查找。这个话题与本主题关系不大,感兴趣的小伙伴能够自行研究。数组

链地址法 为何要把链地址法单独拿出来呢?由于后面有用。 链地址法的大概思想是:对于每一个关键字,使用哈希函数肯定其在哈希表中位置(也就是下标),若是该位置没有元素则直接映射到该地址;若是该位置已经有元素了,就把该元素链接到已存在元素的尾部,也就是一个链表,并把该元素的next设置为null。这样的话,每一个哈希表的位置均可能存在一个链表,这种方式要查找某个元素效率比较高,时间复杂度为O(1+a),a为哈希表中每一个位置链表的平均长度。这里须要假设每一个元素都被等可能映射到哈希表中的任意一个位置。安全

下面这张图展现了链地址法的过程: HashMap HashMap底层实现 HashMap容许使用null做为key或者value,而且HashMap不是线程安全的,除了这两点外,HashMap与Hashtable大体相同,下面是官方API对HashMap的描述:数据结构

Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.多线程

若是有多个线程对Hash映射进行访问,那么至少有一个线程会对哈希映射进行结构的修改:并发

结构上的修改是指添加或删除一个或多个映射关系的任何操做;仅改变与实例已经包含的键关联的值不是结构上的修改ide

那么很显然,当多个线程同时(严格来讲不能称为同时,由于CPU每次只能容许一个线程获取资源,只不过期间上很是短,CPU运行速度很快,因此理解为同时)修改哈希映射,那么最终的哈希映射(就是哈希表)的最终结果是不能肯定的,这只能看CPU心情了。若是要解决这个问题,官方的参考方案是保持外部同步,什么意思?看下面的代码就知道了:函数

Map m = Collections.synchronizedMap(new HashMap(...)); 1 可是不建议这么使用,由于当多个并发的非同步操做修改哈希表的时候,最终结果不可预测,因此使用上面的方法建立HashMap的时候,当有多个线程并发访问哈希表的状况下,会抛出异常,因此并发修改会失败。ui

若是有多个线程执行put方法,并调用resize方法,那么就会出现多种状况,在转移的过程当中丢失数据,或者扩容失败,都有可能,因此从源码的角度分析这也是线程不安全的。

Hashtable和ConcurrentHashMap Hashtable的底层实现 在介绍HashMap提到Hashtable是线程安全的,那么H啊时table是如何实现线程安全的呢?有了上面的介绍,咱们直接从源码中分析其线程安全性:

public synchronized V put(K key, V value) { // 保证value值不为空,此处省略其代码 // 保证key是不重复的,此处省略其代码 //查过阈值则扩容,此处省略 // Creates the new entry. Entry<K,V> e = tab[index]; tab[index] = new Entry<>(hash, key, value, e); count++; return null; } 1 2 3 4 5 6 7 8 9 10 经过源码能够很明显看到其put方法使用synchronized关键字,在线程中这是实现线程安全的一种方式,因此Hashtable是线程安全的。

ConcurrentHashMap的底层实现 ConcurrentHashMap支持彻底并发的对哈希表的操做,ConcurrentHashMap听从了和Hashtable同样的规范,这里指的是线程安全的规范,可是其底层的实现与Hashtable并不一致。ConcurrentHashMap底层采用的锁机制,执行put方法的线程会得到锁,只有当此线程的put方法执行结束后才会释放锁,根据多线程的知识,得到锁的线程会通知其余试图操做put方法的线程,并通知其余线程出于等待状态,直到释放锁后,其余线程才会去从新竞争锁。这一点保证了ConcurrentHashMap的线程安全。

注:这里涉及到了线程锁的知识,若是对这块内容不熟悉,能够参考API。 引用一段官方API对ConcurrentHashMap的描述:

A hash table supporting full concurrency of retrievals and adjustable expected concurrency for updates. This class obeys the same functional specification as Hashtable, and includes versions of methods corresponding to each method of Hashtable. However, even though all operations are thread-safe, retrieval operations do not entail locking, and there is not any support for locking the entire table in a way that prevents all access. This class is fully interoperable with Hashtable in programs that rely on its thread safety but not on its synchronization details.

从这段描述能够看出,ConcurrentHashMap其实是Hashtable的升级版,除了具有线程安全外还增长了迭代器快速失败行为的异常处理,也就是说,经过ConcurrentHashMap对Iterator迭代器结构的修改不会抛出异常,而Hashtable会抛出异常,于是就Hashtable来讲,若是迭代器修改了映射结构,那么遍历的结果是不肯定的,而ConcurrentHashmap支持之容许一个线程对迭代器的映射结构进行修改。

那么咱们接着从源码的角度分析ConcurrentHashMap是如何实现线程安全的:

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 // nonvolatile; recheck (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment s = ensureSegment(j); return s.put(key, hash, value, false); } 1 2 3 4 5 6 7 8 9 10 11 ConcurrentHashMap把要放入的数据分红了多段数据,而后对每段的put操做进行加锁,下面看一下ensureSegment方法:

private Segment<K,V> ensureSegment(int k) { final Segment<K,V>[] ss = this.segments; long u = (k << SSHIFT) + SBASE; // raw offset Segment<K,V> seg; if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { Segment<K,V> proto = ss[0]; // use segment 0 as prototype int cap = proto.table.length; float lf = proto.loadFactor; int threshold = (int)(cap * lf); HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap]; if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { // recheck Segment<K,V> s = new Segment<K,V>(lf, threshold, tab); while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s)) break; } } } return seg; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 这段代码的做用就是根据给定的索引,返回某个具体的Segment,而后根据返回的Segment(块)加锁执行put方法。 再看s.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 { //此处省略详细的处理过程 } } finally { unlock(); } return oldValue; } 1 2 3 4 5 6 7 8 9 10 11 12 在上面的源码中出现了Segment s,咱们来看看它何方神圣:

Segments are specialized versions of hash tables. This subclasses from ReentrantLock opportunistically, just tosimplify some locking and avoid separate construction.

从这段注释中能够发现每次执行ConcurrentHashMap的put方法都是调用s.put()方法的,而Segments对象是一个继承了ReentrantLock锁对象的子类,那么剩下的就很清晰了,每个Segments都有一个锁,只有执行完上面try语句块中的代码才会释放锁,从而保证了多线程并发访问的安全性。

下面来看看ConcurrentHashMap的get方法

public V get(Object key) { Segment<K,V> s; // manually integrate access methods to reduce overhead HashEntry<K,V>[] tab; int h = hash(key); long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) { for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); e != null; e = e.next) { K k; if ((k = e.key) == key || (e.hash == h && key.equals(k))) return e.value; } } return null; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 get操做会经过key找到哈希表的哈希值,根据哈希值定位到某个Segment,而后再从Segment中返回value

小结 HashMap采用链地址法解决哈希冲突,多线程访问哈希表的位置并修改映射关系的时候,后执行的线程会覆盖先执行线程的修改,因此不是线程安全的 Hashtable采用synchronized关键字解决了并发访问的安全性问题可是效率较低 ConcurrentHashMap使用了线程锁分段技术,每次访问只容许一个线程修改哈希表的映射关系,因此是线程安全的 本文大部份内容转自这篇博客http://blog.csdn.net/sbq63683210/article/details/51679790

相关文章
相关标签/搜索