为什么HashMap是线程不安全的,实际会如何体现?--转发

2019-03-02  本文已影响0人  一剑绝尘_bf68

来源 http://www.cnblogs.com/zlslch/p/7634819.html
 
首先,HashMap不支持线程的同步。

同步,指的是在一个时间点只能有一个线程可以修改hash表,任何线程在执行Hashtable的更新操作前都需要获取对象锁,其他线程则等待锁的释放。

第一,如果多个线程同时使用put方法添加元素。

假设正好存在两个put的key发生了碰撞(hash值一样),那么根据HashMap的实现,这两个key会添加到数组的同一个位置,这样最终就会发生其中一个线程的put的数据被覆盖。

第二,如果多个线程同时检测到元素个数超过数组大小*loadFactor。

这样会发生多个线程同时对hash数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给table,也就是说其他线程的都会丢失,并且各自线程put的数据也丢失。且会引起死循环的错误。

HashMap底层是一个Entry数组,当发生hash冲突的时候,hashmap是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入

我们来分析一下多线程访问:

1、在hashmap做put操作的时候会调用下面方法:

[ 复制代码

](javascript:void(0); "复制代码")

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">// 新增Entry。将“key-value”插入指定位置,bucketIndex是位置索引。
void addEntry(int hash, K key, V value, int bucketIndex) { // 保存“bucketIndex”位置的值到“e”中
Entry<K,V> e = table[bucketIndex]; // 设置“bucketIndex”位置的元素为“新Entry”, // 设置“e”为“新Entry的下一个节点”
table[bucketIndex] = new Entry<K,V>(hash, key, value, e); // 若HashMap的实际大小 不小于 “阈值”,则调整HashMap的大小
if (size++ >= threshold)
resize(2 * table.length);
}</pre>

[ 复制代码

](javascript:void(0); "复制代码")

在hashmap做put操作的时候会调用到以上的方法。现在假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失。

2、删除键值对的代码

[ 复制代码

](javascript:void(0); "复制代码")

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;"><span style="font-size: 18px;"> </span>// 删除“键为key”的元素
final Entry<K,V> removeEntryForKey(Object key) { // 获取哈希值。若key为null,则哈希值为0;否则调用hash()进行计算
int hash = (key == null) ? 0 : hash(key.hashCode()); int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev; // 删除链表中“键为key”的元素 // 本质是“删除单向链表中的节点”
while (e != null) {
Entry<K,V> next = e.next;
Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--; if (prev == e)
table[i] = next; else prev.next = next;
e.recordRemoval(this); return e;
}
prev = e;
e = next;
} return e;
}</pre>

[ 复制代码

](javascript:void(0); "复制代码")

当多个线程同时操作同一个数组位置的时候,也都会先取得现在状态下该位置存储的头结点,然后各自去进行计算操作,之后再把结果写会到该数组位置去,其实写回的时候可能其他的线程已经就把这个位置给修改过了,就会覆盖其他线程的修改。

3、addEntry中当加入新的键值对后键值对总数量超过门限值的时候会调用一个resize操作,代码如下:

[ 复制代码

](javascript:void(0); "复制代码")

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">// 重新调整HashMap的大小,newCapacity是调整后的容量
void resize(int newCapacity) {
Entry[] oldTable = table; int oldCapacity = oldTable.length; //如果就容量已经达到了最大值,则不能再扩容,直接返回
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE; return;
} // 新建一个HashMap,将“旧HashMap”的全部元素添加到“新HashMap”中, // 然后,将“新HashMap”赋值给“旧HashMap”。
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}</pre>

[ 复制代码

](javascript:void(0); "复制代码")

这个操作会新生成一个新的容量的数组,然后对原数组的所有键值对重新进行计算和写入新的数组,之后指向新生成的数组。

  当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作,各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的table作为原始数组,这样也会有问题。

如何实现HashMap的同步?

答:

 第一种方法:

   直接使用Hashtable,但是当一个线程访问HashTable的同步方法时,其他线程如果也要访问同步方法,会被阻塞住。举个例子,当一个线程使用put方法时,另一个线程不但不可以使用put方法,连get方法都不可以,效率很低,现在基本不会选择它了。

 第二种方法: HashMap可以通过下面的语句进行同步,

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">Collections.synchronizeMap(hashMap);</pre>

HashMap可以通过Map m = Collections.synchronizedMap(new HashMap())来达到同步的效果

具体而言,该方法返回一个同步的Map,该Map封装了底层的HashMap的所有方法,使得底层的HashMap即使是在多线程的环境中也是安全的。

  第三种方法:

直接使用JDK 5 之后的 ConcurrentHashMap,如果使用Java 5或以上的话,请使用ConcurrentHashMap

  Collections.synchronizeMap(hashMap); 又是如何保证了HashMap线程安全?

[ 复制代码

](javascript:void(0); "复制代码")

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">// synchronizedMap方法
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) { return new SynchronizedMap<>(m);
} // SynchronizedMap类
private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable { private static final long serialVersionUID = 1978198479659022715L; private final Map<K,V> m; // Backing Map
final Object mutex; // Object on which to synchronize
SynchronizedMap(Map<K,V> m) { this.m = Objects.requireNonNull(m);
mutex = this;
}

   SynchronizedMap(Map<K,V> m, Object mutex) { this.m = m; this.mutex = mutex;
   } public int size() {
       synchronized (mutex) {return m.size();}
   } public boolean isEmpty() {
       synchronized (mutex) {return m.isEmpty();}
   } public boolean containsKey(Object key) {
       synchronized (mutex) {return m.containsKey(key);}
   } public boolean containsValue(Object value) {
       synchronized (mutex) {return m.containsValue(value);}
   } public V get(Object key) {
       synchronized (mutex) {return m.get(key);}
   } public V put(K key, V value) {
       synchronized (mutex) {return m.put(key, value);}
   } public V remove(Object key) {
       synchronized (mutex) {return m.remove(key);}
   } // 省略其他方法

}</pre>

[ 复制代码

](javascript:void(0); "复制代码")

从源码中看出 synchronizedMap()方法返回一个SynchronizedMap类的对象,而在SynchronizedMap类中使用了synchronized来保证对Map的操作是线程安全的,故效率其实也不高。

作者:大数据和人工智能躺过的坑
出处:http://www.cnblogs.com/zlslch/

上一篇下一篇

猜你喜欢

热点阅读