Po pierwsze, to nie jest zepsuty niezawodnie. Udało mi się wykonać kilka uruchomień bez żadnego wyjątku. To jednak nie oznacza, że wynikowa mapa jest prawidłowa. Możliwe jest również, że każda nić świadczy o pomyślnym umieszczeniu własnej wartości, a wynikowa mapa pomija kilka mapowań.
Rzeczywiście, nieudana próba z NullPointerException
zdarza się dość często. I stworzył poniższy kod diagnostyczny do zilustrowania HashMap
„s pracy:
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;
}
Stosując to z prostego sekwencyjnego for(int i=0; i<5; i++) debugPut(m, i, i);
drukowanej:
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
Jak widać, z powodu początkowej pojemności 0
, istnieją trzy różne tablice podkładowe utworzone nawet podczas operacji sekwencyjnej. Za każdym razem, gdy pojemność jest zwiększana, istnieje większa szansa, że niezręczny współbieżny put
pominie aktualizację tablicy i utworzy własną tablicę.
Jest to szczególnie istotne w początkowym stanie pustej mapy i kilku wątków próbujących umieścić swój pierwszy klucz, ponieważ wszystkie wątki mogą napotkać początkowy stan tabeli null
i utworzyć własne. Ponadto, nawet podczas odczytu stanu ukończonego pierwszego put
, jest również nowa tablica utworzona dla drugiego put
.
Ale krok po kroku debugowania ujawniła jeszcze więcej szans na łamanie:
Inside the method putVal
widzimy na koniec:
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
Innymi słowy, po pomyślne Wstawienie nowy klucz, tabela zostanie zmieniona, jeśli nowy rozmiar przekroczy threshold
. Tak więc na pierwszym put
, resize()
jest nazywany na początku, ponieważ tabela jest null
a ponieważ Twój określona początkowa pojemność jest 0
, czyli zbyt niskie, aby zapisać jedno mapowanie, nowa pojemność będzie 1
i nowy threshold
będzie 1 * loadFactor == 1 * 0.75f == 0.75f
, w zaokrągleniu do 0
. Tak więc na samym końcu pierwszego put
, nowy threshold
został przekroczony i uruchomiono inną operację resize()
. Tak więc z początkową wydajnością 0
, pierwszy put
już tworzy i zapełnia dwie tablice, co daje znacznie większe szanse na zerwanie, jeśli wiele wątków wykonuje tę akcję jednocześnie, wszystkie napotykają stan początkowy.
I jest jeszcze jeden punkt. Patrząc into the resize()
operation widzimy 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)
Innymi słowy, nowa referencja tablica jest przechowywana w stercie przed została wypełniona starych wpisów, więc nawet bez zmiany kolejności czyta i pisze, jest szansa, że inny wątek odczyta to odwołanie, nie widząc starych wpisów, w tym tego, który sam napisał wcześniej. W rzeczywistości optymalizacje zmniejszające dostęp do sterty mogą obniżyć szanse na to, że wątek nie zobaczy własnej aktualizacji w następnym zapytaniu.
Należy jednak zauważyć, że założenie, że wszystko działa interpretowane tutaj, nie jest uzasadnione. Od HashMap
również wewnętrznie używany JRE, nawet przed uruchomieniem aplikacji, istnieje również możliwość napotkania już skompilowanego kodu podczas korzystania z HashMap
.
Czy próbowałeś ustawić punkt przerwania wyjątków, który zawiesza całą maszynę JVM? może to pozwoli ci sprawdzić stan grafu obiektu – the8472
Wiem, co znajdę --- Wewnętrzny łańcuch kolizji HashMap nie ma instancji 'Węzeł 'odpowiadającej temu, co właśnie wstawiłem. Problem polega na scenariusz, który generuje ten wynik na procesorze Intela (którego macierzysty model pamięci jest dość mocny) bez zakładania równoczesnej realizacji wielordzeniowej. –
"Wartość" nie jest ostateczna, więc może jest to przypadek wydania niebezpiecznej publikacji? – the8472