Ответ 1
Во-первых, это не безупречно. Мне удалось выполнить несколько прогонов, где не было исключений. Это, однако, не означает, что результирующая карта верна. Также возможно, что каждая нить свидетельствует о том, что ее собственная ценность успешно помещена, в то время как итоговая карта пропускает несколько отображений.
Но действительно, неудача с NullPointerException
происходит довольно часто. Я создал следующий код отладки, чтобы проиллюстрировать работу HashMap
s:
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;
}
Используя это с помощью простого последовательного for(int i=0; i<5; i++) debugPut(m, i, i);
, напечатанного:
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
Как видите, из-за начальной емкости 0
существует три разных массива поддержки, созданных даже во время последовательной операции. Каждый раз, когда емкость увеличивается, существует более высокая вероятность того, что краткий параллельный put
пропускает обновление массива и создает свой собственный массив.
Это особенно актуально для исходного состояния пустой карты и нескольких потоков, пытающихся поместить свой первый ключ, поскольку все потоки могут столкнуться с начальным состоянием таблицы null
и создавать свои собственные. Кроме того, даже когда вы читаете состояние завершенного первого put
, для второго put
создается новый массив.
Но пошаговая отладка выявила еще больше шансов на нарушение:
Внутри метода putVal
мы видим в конце:
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
Другими словами, после успешной установки нового ключа таблица будет изменена, если новый размер превышает threshold
. Таким образом, при первом put
, resize()
вызывается в начале, потому что таблица null
, и поскольку указанная начальная емкость 0
, т.е. слишком низкая, чтобы сохранить одно сопоставление, новая емкость будет 1
а новый threshold
будет 1 * loadFactor == 1 * 0.75f == 0.75f
, округлен до 0
. Итак, в конце первого put
, новый threshold
превышен, а другая операция resize()
запущена. Таким образом, с первой емкостью 0
первая put
уже создает и заполняет два массива, что дает гораздо более высокие шансы сломаться, если несколько потоков выполняют это действие одновременно, все сталкиваются с начальным состоянием.
И есть еще один момент. Если посмотреть в операцию resize()
, мы видим строки:
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
… (transfer old contents to new array)
Другими словами, ссылка на новый массив хранится в куче до того, как она будет заполнена старыми записями, поэтому даже без переупорядочения чтений и записей существует вероятность того, что другой поток прочитает эту ссылку, не увидев старые записи, включая тот, который он написал ранее. Фактически, оптимизация, уменьшающая доступ к куче, может снизить вероятность того, что поток не увидит свое собственное обновление в следующем запросе.
Тем не менее, следует также отметить, что предположение о том, что все работает интерпретируется здесь, не основано. Поскольку HashMap
также используется JRE внутри, даже до запуска вашего приложения, есть возможность встретить уже скомпилированный код при использовании HashMap
.