Почему ConcurrentHashMap предотвращает нулевые ключи и значения?

145

В JavaDoc ConcurrentHashMapговорится следующее:

Как , Hashtableно в отличие от HashMap, этот класс никак не позволяет nullиспользовать в качестве ключа или значения.

Мой вопрос: почему?

2-й вопрос: почему не Hashtableразрешено null?

Я использовал много HashMaps для хранения данных. Но при переходе на ConcurrentHashMapнесколько раз у меня возникали проблемы из-за NullPointerExceptions.

Марсель
источник
1
Я считаю, что это крайне досадное несоответствие. EnumMap также не допускает null. Очевидно, что нет технических ограничений, запрещающих использование пустых ключей. для Map <K, V> просто поле с V-типом обеспечит поддержку нулевых ключей (возможно, другое логическое поле, если вы хотите различать нулевое значение и отсутствие значения).
РЭЙ
6
Лучше задать вопрос: «Почему HashMap допускает нулевой ключ и нулевые значения?». Или, возможно, «почему Java позволяет нулю заселять все типы?» Или даже «почему в Java вообще есть нули?».
Джед Уэсли-Смит,

Ответы:

224

От самого автора ConcurrentHashMap(Дуга Ли) :

Основная причина того, что значения NULL не допускаются в ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps), заключается в том, что неоднозначности, которые могут быть едва терпимыми в непараллельных картах, не могут быть учтены. Главный из них заключается в том, что если map.get(key)возвращается null, вы не можете определить, отображается ли ключ явно в nullvs, ключ не отображается. В непараллельной карте вы можете проверить это через map.contains(key), но в параллельной карта могла измениться между вызовами.

Бруно
источник
8
Спасибо, а как насчет использования null в качестве ключа?
AmitW
2
почему бы не использовать Optionals в качестве значений для внутреннего использования
Benez
3
@benez Optional- это функция Java 8, которая тогда была недоступна (Java 5). Вы Optionalдействительно можете использовать s сейчас.
Бруно
@AmitW, я думаю, что ответ такой же, т.е. двусмысленность. Например, предположим, что один поток делает ключ нулевым и сохраняет для него значение. Затем другой поток изменил другой ключ на null. Когда второй поток пытается добавить новое значение, оно будет заменено. Если второй поток пытается получить значение, он получит значение для другого ключа, измененного первым. Таких ситуаций следует избегать.
Декстер
45

Я считаю , что это, по крайней мере частично, позволяют объединять containsKeyи getв один вызов. Если карта может содержать значения NULL, невозможно определить, getвозвращает ли значение NULL, потому что не было ключа для этого значения или просто потому, что значение было нулевым.

Почему это проблема? Потому что нет безопасного способа сделать это самостоятельно. Возьмите следующий код:

if (m.containsKey(k)) {
   return m.get(k);
} else {
   throw new KeyNotPresentException();
}

Поскольку mэто одновременное отображение, ключ к может быть удален между containsKeyи getвызовов, в результате чего этот фрагмент кода , чтобы возвратить нуль , что никогда не был в таблице, а не желаемое KeyNotPresentException.

Обычно вы решаете это путем синхронизации, но с параллельной картой это, конечно, не сработает. Следовательно, подпись для getдолжна была быть изменена, и единственный способ сделать это обратно совместимым способом - не допустить, чтобы пользователь вставлял нулевые значения в первую очередь, и продолжать использовать это как заполнитель для «ключ не найден».

Алиса Перселл
источник
Вы можете это сделать map.getOrDefault(key, NULL_MARKER). Если да null, то значение было null. Если он возвращается NULL_MARKER, значение не было.
Oliv
@Oliv Только с Java 8. Кроме того, для этого типа может не быть разумного нулевого маркера.
Элис Перселл
@AlicePurcell, «но с параллельной картой это, конечно, не сработает» - почему, я могу синхронизироваться с параллельной версией аналогичным образом - поэтому мне интересно, почему это не сработает. Вы можете это уточнить.
samshers 01
@samshers Никакие операции с параллельной картой не синхронизируются, поэтому вам потребуется внешняя синхронизация всех вызовов, и в этот момент вы не только потеряли все преимущества одновременной карты в производительности, но и оставили ловушку для будущих сопровождающих, которые естественно ожидать безопасного доступа к параллельной карте без синхронизации.
Элис Перселл
@AlicePurcell, отлично. Хотя технически возможно, это определенно станет кошмаром обслуживания, и никакие последующие пользователи не будут ожидать, что им придется синхронизировать параллельную версию.
samshers
4

Дизайн Джоша Блоха HashMap; Дизайн Дуга Ли ConcurrentHashMap. Надеюсь, это не клевета. На самом деле я думаю, что проблема в том, что нули часто требуют переноса, чтобы реальный ноль мог обозначать неинициализированный. Если клиентскому коду требуются значения NULL, он может оплатить (по общему признанию небольшую) стоимость самой упаковки NULL.

Том Хотин - tackline
источник
3

Вы не можете синхронизировать по нулю.

Изменить: это не совсем то, почему в этом случае. Сначала я подумал, что происходит что-то необычное с блокировкой чего-либо против одновременных обновлений или иным образом с использованием монитора объектов для определения того, было ли что-то изменено, но после изучения исходного кода оказалось, что я ошибался - они блокируют использование «сегмента» на основе битовая маска хеша.

В этом случае я подозреваю, что они сделали это для копирования Hashtable, и я подозреваю, что Hashtable сделал это, потому что в мире реляционных баз данных null! = Null, поэтому использование null в качестве ключа не имеет смысла.

Пол Томблин
источник
1
А? Синхронизация ключей и значений карты не выполняется. В этом не было бы смысла.
Тобиас Мюллер,
Есть и другие виды блокировок. Вот что делает его «одновременным». Для этого ему нужен объект, на котором можно держаться.
Пол Томблин,
2
Почему внутри нет специального объекта, который можно было бы использовать для синхронизации нулевых значений? например, «частный объект NULL = новый объект ();». Думаю, я видел это раньше ...
Марсель
Какие еще виды блокировки вы имеете в виду?
Тобиас Мюллер,
На самом деле, теперь, когда я смотрю исходный код gee.cs.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/concurrent/ ... У меня есть серьезные сомнения на этот счет . Похоже, он использует блокировку сегментов, а не блокировку отдельных элементов.
Пол Томблин,
0

ConcurrentHashMap является потокобезопасным. Я считаю, что запрет на использование пустых ключей и значений был частью обеспечения потоковой безопасности.

Кевин Кроуэлл
источник
0

Я предполагаю, что следующий фрагмент документации API дает хороший намек: «Этот класс полностью совместим с Hashtable в программах, которые полагаются на его безопасность потоков, но не на его детали синхронизации».

Вероятно, они просто хотели сделать ConcurrentHashMapполностью совместимыми / взаимозаменяемыми Hashtable. А так Hashtableже не допускает нулевых ключей и значений ..

Тобиас Мюллер
источник
2
И почему Hashtable не поддерживает null?
Марсель
Глядя на его код, я не вижу очевидной причины, по которой Hashtable не допускает нулевых значений. Может, это было просто решение API, когда создавался класс ?! HashMap имеет специальную внутреннюю обработку для нулевого регистра, чего нет в Hashtable. (Он всегда выдает исключение NullPointerException.)
Тобиас Мюллер,
-3

Я не думаю, что запрет на нулевое значение - правильный вариант. Во многих случаях мы действительно хотим поместить ключ с нулевым значением в текущую карту. Однако, используя ConcurrentHashMap, мы не можем этого сделать. Я предполагаю, что будущая версия JDK может это поддерживать.

Иньхаомин
источник
1
Думали ли вы о том, чтобы побороться за Джавачемпиона?
BlackBishop
Используйте Optional, если вы хотите, чтобы ваши ключи имели нулевое поведение.
Элис Перселл