HashMapのあなたは、高い同時実行ではいくつかの問題、どのような可能性のある問題に遭遇します
はじめに:
我々はすべて知っているように、HashMapの問題は、並行環境で発生することがありますが、特定の性能は、並行性の問題が発生すると、なぜ、
誰もが理解かもしれませんが、この記事では、マルチスレッド環境でHashMapを生じる可能性がある問題について記録し、どのように回避します。
HashMapの並行性の問題を分析する前に、まず簡単な基本的な動作のHashMapが達成されて入れて取得する方法を学びます。
入れてもらう操作の1.HashMap
私たちは皆、つまり、リストの構造によって、同じ配列位置に2つのハッシュ値を保存し、達成するために、内部HashMapのは、ジッパーハッシュ法による紛争を解決するためのものであることを知っています
HashMapの独自のハッシュ関数におけるキーの操作する主宣告空、ハッシュコードの実装を入れ、bucketindex位置、およびキー操作の繰り返しカバレッジを取得します。
特定の操作を置く方法のコントロールソースコードの解析が行われます。
関連するいくつかの方法:
静的INTハッシュ(int型H){ H ^ =(H >>> 20)^(H >>> 12)。 H ^(H >>> 7)^(H >>> 4)を返します。 } 静的INT indexFor(INT H、INT長){ H&(長さ1)を返します。 }
データプットが完了した後、それを取得する方法である、我々は、機能の動作を見てもらいます。
次のノードのキー、値、ハッシュ値に対応するキーのリストを含む4つのフィールドを、格納されたノード・データ構造のリストを見てください。
静的クラスエントリ<K、Vは>のMap.Entry <K、V>を実装{ 最後のKキー、キー//キーと値の構造 V値; //ストア値 エントリ<K、V>次; //次のリストノードを指し 最終int型のハッシュ; //ハッシュ値 }
2.Rehash /再ハッシュアレイの内部長さを延長します
哈希表结构是结合了数组和链表的优点,在最好情况下,查找和插入都维持了一个较小的时间复杂度O(1),
不过结合HashMap的实现,考虑下面的情况,如果内部Entry[] tablet的容量很小,或者直接极端化为table长度为1的场景,那么全部的数据元素都会产生碰撞,
这时候的哈希表成为一条单链表,查找和添加的时间复杂度变为O(N),失去了哈希表的意义。
所以哈希表的操作中,内部数组的大小非常重要,必须保持一个平衡的数字,使得哈希碰撞不会太频繁,同时占用空间不会过大。
这就需要在哈希表使用的过程中不断的对table容量进行调整,看一下put操作中的addEntry()方法:
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); }
这里面resize的过程,就是再散列调整table大小的过程,默认是当前table容量的两倍。
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; //初始化一个大小为oldTable容量两倍的新数组newTable transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); }
关键的一步操作是transfer(newTable),这个操作会把当前Entry[] table数组的全部元素转移到新的table中,
这个transfer的过程在并发环境下会发生错误,导致数组链表中的链表形成循环链表,在后面的get操作时e = e.next操作无限循环,Infinite Loop出现。
下面具体分析HashMap的并发问题的表现以及如何出现的。
3.HashMap在多线程put后可能导致get无限循环
HashMap在并发环境下多线程put后可能导致get死循环,具体表现为CPU使用率100%,
看一下transfer的过程:
这里引用酷壳陈皓的博文:
并发下的Rehash
1)假设我们有两个线程。我用红色和浅蓝色标注了一下。
我们再回头看一下我们的 transfer代码中的这个细节:
而我们的线程二执行完成了。于是我们有下面的这个样子。
注意,因为Thread1的 e 指向了key(3),而next指向了key(7),其在线程二rehash后,指向了线程二重组后的链表。我们可以看到链表的顺序被反转后。
2)线程一被调度回来执行。
先是执行 newTalbe[i] = e;
然后是e = next,导致了e指向了key(7),
而下一次循环的next = e.next导致了next指向了key(3)
3)一切安好。
线程一接着工作。把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移。
4)环形链接出现。
e.next = newTable[i] 导致 key(3).next 指向了 key(7)
注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。
于是,当我们的线程一调用到,HashTable.get(11)时,悲剧就出现了——Infinite Loop。
针对上面的分析模拟这个例子,
这里在run中执行了一个自增操作,i++非原子操作,使用AtomicInteger避免可能出现的问题:
public static void main(String[] args){ MapThread t0 = new MapThread(); MapThread t1 = new MapThread(); // 省略 t2-t9 t0.start(); t1.start(); // 省略 t2-t9 }
注意并发问题并不是一定会产生,可以多执行几次,
我试验了上面的代码很容易产生无限循环,控制台不能终止,有线程始终在执行中,
这是其中一个死循环的控制台截图,可以看到六个线程顺利完成了put工作后销毁,还有四个线程没有输出,卡在了put阶段,感兴趣的可以断点进去看一下:
上面的代码,如果把注释打开,换用ConcurrentHashMap就不会出现类似的问题。
4.多线程put的时候可能导致元素丢失
HashMap另外一个并发可能出现的问题是,可能产生元素丢失的现象。
考虑在多线程下put操作时,执行addEntry(hash, key, value, i),如果有产生哈希碰撞,
导致两个线程得到同样的bucketIndex去存储,就可能会出现覆盖丢失的情况:
5.使用线程安全的哈希表容器
那么如何使用线程安全的哈希表结构呢,这里列出了几条建议:
使用Hashtable 类,Hashtable 是线程安全的;
使用并发包下的java.util.concurrent.ConcurrentHashMap,ConcurrentHashMap实现了更高级的线程安全;
または、スレッドセーフな地図を取得し、包装のHashMapオブジェクトを同期させるためsynchronizedMap()メソッドを使用して、この地図上で動作