Erstens, es ist nicht zuverlässig ausfällt. Ich habe es geschafft, einige Runs zu haben, bei denen keine Ausnahme aufgetreten ist. Dies bedeutet jedoch nicht, dass die resultierende Karte korrekt ist. Es ist auch möglich, dass jeder Thread seinen eigenen Wert erfolgreich platziert, während die resultierende Map mehrere Zuordnungen verpasst.
Aber in der Tat passiert oft mit einer NullPointerException
scheitern. Ich habe den folgenden Debug-Code die HashMap
‚s Arbeiten zu veranschaulichen:
static <K,V> void debugPut(HashMap<K,V> m, K k, V v) {
if(m.isEmpty()) debug(m);
m.put(k, v);
debug(m);
}
private static <K, V> void debug(HashMap<K, V> m) {
for(Field f: FIELDS) try {
System.out.println(f.getName()+": "+f.get(m));
} catch(ReflectiveOperationException ex) {
throw new AssertionError(ex);
}
System.out.println();
}
static final Field[] FIELDS;
static {
String[] name={ "table", "size", "threshold" };
Field[] f=new Field[name.length];
for (int ix = 0; ix < name.length; ix++) try {
f[ix]=HashMap.class.getDeclaredField(name[ix]);
}
catch (NoSuchFieldException ex) {
throw new ExceptionInInitializerError(ex);
}
AccessibleObject.setAccessible(f, true);
FIELDS=f;
}
Mit diesem mit den einfachen sequentiellen for(int i=0; i<5; i++) debugPut(m, i, i);
gedruckt:
table: null
size: 0
threshold: 1
table: [Ljava.util.HashMap$Node;@70dea4e
size: 1
threshold: 1
table: [Ljava.util.HashMap$Node;@5c647e05
size: 2
threshold: 3
table: [Ljava.util.HashMap$Node;@5c647e05
size: 3
threshold: 3
table: [Ljava.util.HashMap$Node;@33909752
size: 4
threshold: 6
table: [Ljava.util.HashMap$Node;@33909752
size: 5
threshold: 6
Wie Sie sehen können, aufgrund der Anfangskapazität von 0
, Es gibt drei verschiedene Backing-Arrays, die sogar während der sequentiellen Operation erstellt wurden. Jedes Mal, wenn die Kapazität erhöht wird, besteht eine höhere Wahrscheinlichkeit, dass ein rassiger gleichzeitiger put
das Array-Update verpasst und ein eigenes Array erstellt.
Dies ist besonders relevant für den Anfangszustand einer leeren Karte und mehr Threads versuchen, ihren ersten Schlüssel zu setzen, da alle Threads könnten den Ausgangszustand eine null
Tabelle begegnen und ihre eigene Gruppe gründen. Auch beim Lesen des Status einer abgeschlossenen ersten put
wird ein neues Array für die zweite put
erstellt.
Aber Schritt-für-Schritt Debuggen noch mehr Chancen zu brechen offenbart:
Inside the method putVal
sehen wir am Ende:
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
Mit anderen Worten, nach die erfolgreiche Einführung von Wenn die neue Größe den Wert threshold
überschreitet, wird die Größe der Tabelle geändert. So auf den ersten put
, resize()
am Anfang genannt wird, da die Tabelle null
ist und seit Ihrer angegebene Anfangskapazität 0
ist, dh zu niedrig ein Mapping zu speichern, wird die neue Kapazität 1
und die neuen threshold
werden 1 * loadFactor == 1 * 0.75f == 0.75f
sein, aufgerundet auf 0
. Also genau am Ende der ersten put
wird die neue threshold
überschritten und ein weiterer resize()
Betrieb ausgelöst. Also mit einer anfänglichen Kapazität von 0
, die erste put
bereits erstellt und bevölkert zwei Arrays, die viel höhere Chancen zu brechen gibt, wenn mehrere Threads diese Aktion gleichzeitig durchführen, alle auf den Ausgangszustand.
Und es gibt einen anderen Punkt. Suchen into the resize()
operation wir sehen the lines:
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
… (transfer old contents to new array)
Mit anderen Worten, wird das neue Array-Referenz in den Heap gespeichert vor es mit den alten Einträgen gefüllt wurde, so dass auch ohne von liest Neuordnen und schreibt, es ist ein Chance, dass ein anderer Thread diese Referenz liest, ohne die alten Einträge zu sehen, einschließlich desjenigen, den er zuvor selbst geschrieben hat. Tatsächlich können Optimierungen, die den Heap-Zugriff reduzieren, die Wahrscheinlichkeit verringern, dass ein Thread seine eigene Aktualisierung in einer unmittelbar folgenden Abfrage nicht sieht.
Trotzdem muss es auch angemerkt werden, dass die Annahme, dass alles hier interpretiert interpretiert wird, nicht begründet ist. Da HashMap
auch von der JRE intern verwendet wird, besteht bereits vor dem Starten der Anwendung die Möglichkeit, bereits kompilierten Code zu finden, wenn Sie HashMap
verwenden.
haben Sie versucht, einen Ausnahme-Breakpoint zu setzen, der die gesamte JVM aussetzt? Vielleicht können Sie damit den Zustand des Objektgraphen überprüfen – the8472
Ich weiß, was ich finden werde --- Die interne Kollisionskette von HashMap fehlt eine 'Node'-Instanz, die der Sache entspricht, die ich gerade eingefügt habe. Das Problem kommt auf ein Szenario, das dieses Ergebnis auf der Intel-CPU erzeugt (dessen natives Speichermodell ziemlich stark ist), ohne gleichzeitige Multicore-Ausführung anzunehmen. –
"Wert" ist nicht endgültig, also wird vielleicht eine unsichere Publikation nachbestellt? – the8472