いくつかの詳細な分析をしながらConcurrentHashMap
、でも言っているインターネット上のブログの記事を見つけConcurrentHashMap
、無限ループに陥る場合があります。
これは、この例を示します。私はこのコードを実行したとき - それは捕まってしまいました。
public class Test {
public static void main(String[] args) throws Exception {
Map<Long, Long> map = new ConcurrentHashMap<>();
map.put(0L, 0L);
map.put((1L << 32) + 1, 0L);
for (long key : map.keySet()) {
map.put(key, map.remove(key));
}
}
}
このデッドロックが発生した理由を説明してください。
他の人がすでに言ったように:それはデッドロックが、無限ループではありません。かかわらず、その、問題の核心(とタイトル)がある:これはなぜ起こるのでしょうか?
他の回答は、ここではあまり詳細には触れませんが、私はより良いだけでなく、これを理解することは興味がありました。例えば、あなたが行を変更
map.put((1L << 32) + 1, 0L);
に
map.put(1L, 0L);
それはないない動けなくなります。そして再び、質問がある理由。
答えは:それは複雑です。
ConcurrentHashMap
唯一の基本的な説明のコメントの230行で、コードのなんと6300本のラインと同時/コレクションフレームワークから最も複雑なクラスの一つ、ある概念の実装のを、なぜ魔法と読めないコードが実際に動作します。以下は、かなり簡略化されているが、少なくとも説明しなければならない基本的な問題を。
まず第一に:によって返されたセットMap::keySet
であるビュー内部状態に。そして、Javadocは言います:
戻り値このマップに含まれるキーのSetビューを。セットはマップによってバックアップされているので、マップへの変更はセットに反映され、およびその逆。セットの反復処理(反復子自身のremoveオペレーションを除く)中にマップが変更された場合、反復の結果は未定義れます。セットがサポートする要素の削除、[...]
(私が強調)
しかし、のJavadocはConcurrentHashMap::keySet
言います:
戻り値このマップに含まれるキーのSetビューを。セットはマップによってバックアップされているので、マップへの変更はセットに反映され、およびその逆。セットがサポートする要素の削除、[...]
(それはないことに注意してくださいません未定義の動作に言及!)
繰り返し処理しながら、通常、マップを変更keySet
投げるでしょうConcurrentModificationException
。しかし、ConcurrentHashMap
これに対処することができます。あなたの場合のように-それは結果がまだ予想外であっても、一貫性のままで、まだ繰り返し処理することができます。
あなたが観測されていること挙動の理由に来ます:
ハッシュテーブル(またはハッシュマップ)は、基本的鍵からハッシュ値を計算し、エントリが追加されるべきであること、「バケツ」の指標として、このキーを使用して動作します。複数のキーが同じバケットにマッピングされている場合は、その後、バケット内のエントリは、通常のように管理されているリンクリスト。同じことがのためのケースですConcurrentHashMap
。
ノードからなるテーブルの特に、「バケット」、 - - 次のプログラムは、テーブルの内部状態を印刷するために、いくつかの厄介な反射ハックを使用し、反復し、修正中:
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class MapLoop
{
public static void main(String[] args) throws Exception
{
runTestInfinite();
runTestFinite();
}
private static void runTestInfinite() throws Exception
{
System.out.println("Running test with inifinite loop");
Map<Long, Long> map = new ConcurrentHashMap<>();
map.put(0L, 0L);
map.put((1L << 32) + 1, 0L);
int counter = 0;
for (long key : map.keySet())
{
map.put(key, map.remove(key));
System.out.println("Infinite, counter is "+counter);
printTable(map);
counter++;
if (counter == 10)
{
System.out.println("Bailing out...");
break;
}
}
System.out.println("Running test with inifinite loop DONE");
}
private static void runTestFinite() throws Exception
{
System.out.println("Running test with finite loop");
Map<Long, Long> map = new ConcurrentHashMap<>();
map.put(0L, 0L);
map.put(1L, 0L);
int counter = 0;
for (long key : map.keySet())
{
map.put(key, map.remove(key));
System.out.println("Finite, counter is "+counter);
printTable(map);
counter++;
}
System.out.println("Running test with finite loop DONE");
}
private static void printTable(Map<Long, Long> map) throws Exception
{
// Hack, to illustrate the issue here:
System.out.println("Table now: ");
Field fTable = ConcurrentHashMap.class.getDeclaredField("table");
fTable.setAccessible(true);
Object t = fTable.get(map);
int n = Array.getLength(t);
for (int i = 0; i < n; i++)
{
Object node = Array.get(t, i);
printNode(i, node);
}
}
private static void printNode(int index, Object node) throws Exception
{
if (node == null)
{
System.out.println("at " + index + ": null");
return;
}
// Hack, to illustrate the issue here:
Class<?> c =
Class.forName("java.util.concurrent.ConcurrentHashMap$Node");
Field fHash = c.getDeclaredField("hash");
fHash.setAccessible(true);
Field fKey = c.getDeclaredField("key");
fKey.setAccessible(true);
Field fVal = c.getDeclaredField("val");
fVal.setAccessible(true);
Field fNext = c.getDeclaredField("next");
fNext.setAccessible(true);
System.out.println(" at " + index + ":");
System.out.println(" hash " + fHash.getInt(node));
System.out.println(" key " + fKey.get(node));
System.out.println(" val " + fVal.get(node));
System.out.println(" next " + fNext.get(node));
}
}
以下のための出力runTestInfinite
次のようにケース(冗長部分は省略)です。
Running test with infinite loop
Infinite, counter is 0
Table now:
at 0:
hash 0
key 4294967297
val 0
next 0=0
at 1: null
at 2: null
...
at 14: null
at 15: null
Infinite, counter is 1
Table now:
at 0:
hash 0
key 0
val 0
next 4294967297=0
at 1: null
at 2: null
...
at 14: null
at 15: null
Infinite, counter is 2
Table now:
at 0:
hash 0
key 4294967297
val 0
next 0=0
at 1: null
at 2: null
...
at 14: null
at 15: null
Infinite, counter is 3
...
Infinite, counter is 9
...
Bailing out...
Running test with infinite loop DONE
一つは、キーのエントリがあることがわかります0
し、キー4294967297
(あなたです(1L << 32) + 1
)常にバケット0で終わり、彼らはリンクリストとして維持されます。以上の繰り返しだから、keySet
この表で始まります:
Bucket : Contents
0 : 0 --> 4294967297
1 : null
... : ...
15 : null
最初の反復では、キーを削除し0
、基本的には、このいずれかにテーブルを回し、:
Bucket : Contents
0 : 4294967297
1 : null
... : ...
15 : null
しかし、鍵は0
直後に追加され、それが同じバケツで終わる4294967297
-それは、リストの末尾に追加されるように:
Bucket : Contents
0 : 4294967297 -> 0
1 : null
... : ...
15 : null
(これはによって示されるnext 0=0
出力の一部)。
次の反復では、4294967297
それが最初にあったのと同じ状態にテーブルをもたらし、除去され、再挿入します。
あなたの無限ループがどこから来ると、それはです。
それとは対照的に、用の出力runTestFinite
の場合はこれです:
Running test with finite loop
Finite, counter is 0
Table now:
at 0:
hash 0
key 0
val 0
next null
at 1:
hash 1
key 1
val 0
next null
at 2: null
...
at 14: null
at 15: null
Finite, counter is 1
Table now:
at 0:
hash 0
key 0
val 0
next null
at 1:
hash 1
key 1
val 0
next null
at 2: null
...
at 14: null
at 15: null
Running test with finite loop DONE
一つは、そのキーを見ることができます0
し、1
で終わる異なるバケット。そう関連する要素(すなわち最初の2つのバケット)を反復した後、そこに取り除かれるいかなるリンクされたリストはありません(および追加)の要素が追加されることができ、ループ終了後。