NPE в Java HashSet.isEmpty ()

Я получил NPE в java.util.HashSet.isEmpty () в строке 191. Я думаю, эта строка просто вызывает isEmpty () во внутреннем поле карты.

К моему удивлению, это поле временное, поэтому после десериализации оно будет нулевым. Но разве не в этом цель десериализации набора, чтобы вернуть значения? На мой взгляд, десериализация - единственный способ, которым это поле может иметь значение NULL для экземпляра HashSet.

Наверное, мне что-то здесь не хватает. Кто-нибудь может объяснить?

Java 1.8.0_151 в Linux (3.13.0-61-generic) на платформе amd64

Вот реализация HashMap.isEmpty из JDK:

/**
 * Returns <tt>true</tt> if this set contains no elements.
 *
 * @return <tt>true</tt> if this set contains no elements
 */
public boolean isEmpty() {
    return map.isEmpty(); // line 191
}

Редактировать / дополнительная информация:

  • К сожалению, я не могу привести минимальный пример, демонстрирующий проблему. Это происходит во время регрессионного теста сложной системы, работающей часами. Итак, я не знаю и не могу контролировать, что происходит с этим набором. Кроме того, проблема недетерминированная, то есть мы видим ее время от времени, но не каждый раз.
  • Я мог бы предоставить фрагмент нашего кода, который вызывает isEmpty(). Но NPE встречается в библиотеке, поэтому этот метод явно был вызван в существующем экземпляре, то есть не в null. Поэтому предоставление этого фрагмента мало поможет.
  • наш код многопоточный. Но на первый взгляд эта часть имеет только однопоточный доступ.
  • наш код перехватывает и проглатывает исключения во многих местах. Это могло скрыть настоящего виновника
private void readObject(java.io.ObjectInputStream s) инициализирует резервную карту, поэтому она не должна быть нулевой после десериализации.
Eran 26.09.2018 10:01

Не могли бы вы сделать небольшой фрагмент кода, демонстрирующий эту проблему? Итак, создайте новый набор, запишите его, а затем прочтите его снова, после чего вызовите isEmpty(), это позволяет нам быстро увидеть проблему.

Ferrybig 26.09.2018 10:06

как насчет предоставления фрагмента кода? потому что я понятия не имею, что происходит в строке 191 (-`

Roee Gavirel 26.09.2018 10:08

Возможно, ваша установленная ссылка нулевая.

khelwood 26.09.2018 10:10

Бросив взгляд на исходный код JDK, я не могу понять, как это могло произойти. Я думал, что если объект может преждевременно выйти из конструктора или readObject, другой поток может вызвать isEmpty до того, как будет установлен map; но я не видел, как это могло случиться.

Ole V.V. 26.09.2018 10:28

Это поле должно быть установлено всегда, однако этого может не быть, если оно было записано в одном потоке и получено другим небезопасным для потоков способом.

Peter Lawrey 26.09.2018 10:28

Правильно, @PeterLawrey, но есть ли способ, чтобы другой поток мог получить ссылку на HashSet до того, как конструктор или readObject установит поле? Или модель памяти Java позволит другому потоку видеть исходный null даже после того, как readObject установил для поля значение, отличное от нуля?

Ole V.V. 26.09.2018 10:34

Ульрих Шольц, один эксперимент, который вы можете попробовать, - это синхронизировать доступ к вашему HashSet, чтобы убедиться, что нет проблем с параллелизмом, и увидеть, что проблема исчезнет.

Ole V.V. 26.09.2018 10:36

Я также видел бесконечные циклы в HashMap#get. Единственный способ, которым это может произойти, - это совместное использование этих контейнеров между потоками без надлежащей синхронизации. Они могут попадать во всевозможные противоречивые состояния. Не делай этого.

Thilo 26.09.2018 10:38

Помимо десериализации, второй путь кода, который сбрасывает map, - это clone.

Thilo 26.09.2018 10:44

@Thilo, но во время clone() это никогда не будет null.

Holger 26.09.2018 10:49

@Holger. Я тоже не понимаю, как это могло быть, но это заставило меня задуматься: является ли clone потокобезопасным? Если вы передадите клон в другой поток, все ли поля будут надежно клонированы?

Thilo 26.09.2018 10:53

@Holger, Thilo: Может ли быть следующее: исключение во время десериализации / во время super.clone (), оставляя экземпляр HashSet в неинициализированном состоянии?

Ulrich Scholz 26.09.2018 10:56

@ OleV.V. правильно, если не использовались барьеры памяти. Вы можете получить ссылку на HashSet, вызвать readObject и по-прежнему видеть нулевое значение во втором потоке. Это зависит от того, как была передана ссылка на набор.

Peter Lawrey 26.09.2018 11:02

@UlrichScholz обычно нет, так как в исключительном случае у вас редко есть шанс получить в руки неполный экземпляр. Для этого потребуется очень сложный сценарий. В качестве первого шага, чтобы сузить проблему, замените любой код перехвата исключений кодом регистрации (или повторной генерации) исключений. Вы все равно должны это сделать. В противном случае за углом поджидает следующая проблема ...

Holger 26.09.2018 11:02

@Holger. Я уже положил эту замену на пластину. Посмотрим, назначат ли мне на это время.

Ulrich Scholz 26.09.2018 11:42
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
16
270
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Поскольку поле map не является final, нет никакой гарантии увидеть его в инициализированном состоянии при неправильной публикации вновь созданного экземпляра HashSet в многопоточном коде. В то время как «вновь созданный» означает, что никаких других действий, обеспечивающих видимость памяти между задействованными потоками, не произошло с момента создания, что в принципе может быть произвольно долгим.

Во время десериализации объекта метод readObject выполняет повторную инициализацию поля map и возвращает элементы. Это необходимо, чтобы скрыть эти детали реализации от постоянной формы. Кроме того, десериализованные объекты могут иметь хэш-код, отличный от хэш-кода, который был у них при сериализации (например, возьмите хеш-код, унаследованный от java.lang.Object). Так что их все равно придется вставлять заново. Общий принцип большинства коллекций - скрыть детали реализации и просто сериализовать и десериализовать каждый содержащийся элемент один за другим с помощью специальных методов writeObject и readObject.

Таким образом, будущая реализация HashSet может быть настоящим хеш-набором, без использования HashMap за кулисами, без ущерба для совместимости с сериализацией.

Часть об изменении hashCode между запусками JVM интересна. Если вы поместите в этот набор что-либо, зависящее от идентичности экземпляра (вероятно, это в первую очередь плохая идея, но есть некоторые варианты использования), это не пройдет через сериализатор. OTOH, если вы изменили реализацию "hashCode" вашего пользовательского класса между перезапусками (вероятно, еще одна плохая идея), десериализатор, повторно хеширующий все, сохранит его согласованность с новой реализацией.

Thilo 26.09.2018 11:32

Даже в рамках одной и той же среды выполнения сериализация с последующей десериализацией подразумевает операцию копирования, которая изменяет идентификаторы объектов, за некоторыми исключениями, такими как константы и одиночные экземпляры enum.

Holger 26.09.2018 11:49

Основная причина - одновременный доступ

Ulrich Scholz 11.10.2018 11:52

Другие вопросы по теме