Учитывая, что HashMaps в jdk1.6 и выше вызывают проблемы с multi = threading, как мне исправить свой код?

83

Недавно я поднял вопрос в stackoverflow, потом нашел ответ. Первоначальный вопрос заключался в том, какие механизмы, кроме мьютексов или сборки мусора, могут замедлить мою многопоточную Java-программу?

К своему ужасу я обнаружил, что HashMap был изменен между JDK1.6 и JDK1.7. Теперь у него есть блок кода, который заставляет все потоки, создающие HashMaps, синхронизироваться.

Строка кода в JDK1.7.0_10:

 /**A randomizing value associated with this instance that is applied to hash code of  keys to make hash collisions harder to find.     */
transient final int hashSeed = sun.misc.Hashing.randomHashSeed(this);

Что в итоге вызывает

 protected int next(int bits) {
    long oldseed, nextseed;
    AtomicLong seed = this.seed;
    do {
        oldseed = seed.get();
        nextseed = (oldseed * multiplier + addend) & mask;
    } while (!seed.compareAndSet(oldseed, nextseed));
    return (int)(nextseed >>> (48 - bits));
 }    

Глядя на другие JDK, я обнаружил, что этого нет в JDK1.5.0_22 или JDK1.6.0_26.

Влияние на мой код огромно. Это делает так, что когда я запускаю 64 потока, я получаю меньше производительности, чем когда я запускаю 1 поток. JStack показывает, что большинство потоков тратят большую часть своего времени на выполнение цикла в случайном порядке.

Так что у меня есть несколько вариантов:

  • Перепишите мой код, чтобы я не использовал HashMap, а использовал что-то подобное
  • Как-нибудь поиграйте с rt.jar и замените хэш-карту внутри него
  • Как-то возиться с путем к классу, поэтому каждый поток получает свою версию HashMap

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

Спасибо за помощь

Посох Эскура
источник
2
Что требует от вас создания такого количества хэш-карт? Что ты пытаешься сделать?
fge
3
2 комментария: 1. ConcurrentHashMap, похоже, не использует это - может ли это быть альтернативой? 2. Этот фрагмент кода вызывается только при создании карты. Это означает, что вы создаете миллионы хэш-карт в условиях высокой конкуренции - действительно ли это отражает реальную производственную нагрузку?
assylias
1
На самом деле ConcurrentHashMap тоже использует этот метод (в oracle jdk 1.7_10), но, по-видимому, openJDK 7 этого не делает .
assylias
1
@assylias Вы должны проверить последнюю версию здесь . У этого действительно есть такая строчка кода.
Марко Топольник
3
@StaveEscura AtomicLongделает ставку на низкий уровень конкуренции за запись, чтобы работать хорошо. У вас высокая конкуренция за запись, поэтому вам нужна регулярная монопольная блокировка. Напишите синхронизированную HashMapфабрику, и вы, вероятно, увидите улучшение, если все, что вы когда-либо делаете в этих потоках, - это создание экземпляров карты.
Марко Топольник

Ответы:

56

Я являюсь первоначальным автором патча, который появился в 7u6, CR # 7118743: Альтернативное хеширование для строки с картами на основе хеширования‌.

Я сразу признаю, что инициализация hashSeed является узким местом, но мы не ожидали, что это будет проблемой, поскольку это происходит только один раз для каждого экземпляра Hash Map. Чтобы этот код стал узким местом, вам придется создавать сотни или тысячи хэш-карт в секунду. Это, конечно, нетипично. Есть ли действительно веская причина для вашего приложения , чтобы делать это? Как долго живут эти хеш-карты?

Несмотря на это, мы, вероятно, рассмотрим переход на ThreadLocalRandom, а не на Random, и, возможно, какой-нибудь вариант отложенной инициализации, предложенный cambecc.

РЕДАКТИРОВАТЬ 3

Исправление узкого места было добавлено в ртутный репозиторий обновлений JDK7:

http://hg.openjdk.java.net/jdk7u/jdk7u-dev/jdk/rev/b03bbdef3a88

Исправление будет частью предстоящего выпуска 7u40 и уже доступно в выпусках IcedTea 2.4.

Почти финальные тестовые сборки 7u40 доступны здесь:

https://jdk7.java.net/download.html

Отзывы по-прежнему приветствуются. Отправьте его на http://mail.openjdk.java.net/mailman/listinfo/core-libs-dev, чтобы убедиться, что разработчики openJDK увидят его.

Майк Дуигу
источник
1
Спасибо, что изучили это. Да, действительно существует необходимость в создании такого количества карт: приложение на самом деле довольно простое, но 100 000 человек могут использовать его за секунду, а это означает, что миллионы карт могут быть созданы очень быстро. Я, конечно, могу переписать его, чтобы не использовать карты, но это очень дорого обходится. На данный момент план использования отражения для взлома случайного поля выглядит неплохим
Stave Escura
2
Майк, предложение по краткосрочному исправлению: кроме ThreadLocalRandom (у которого будут свои проблемы с приложениями, которые возятся с локальным хранилищем потоков), не было бы намного проще и дешевле (с точки зрения времени, риска и тестирования) stripe Hashing.Holder.SEED_MAKER в массив (скажем) <num cores> Random экземпляров и использовать идентификатор вызывающего потока для% -index в нем? Это должно мгновенно уменьшить (но не устранить) конкуренцию за поток без каких-либо заметных побочных эффектов.
Holger Hoffstätte,
10
Веб-приложения @mduigou, которые имеют высокую частоту запросов и используют JSON, будут создавать большое количество HashMaps в секунду, поскольку большинство, если не все библиотеки JSON используют HashMaps или LinkedHashMaps для десериализации объектов JSON. Веб-приложения, использующие JSON, широко распространены, и создание HashMaps может не контролироваться приложением (а может использоваться библиотечными приложениями), поэтому я бы сказал, что есть веские причины не создавать узких мест при создании HashMaps.
sbordet 06
3
@mduigou, возможно, простое облегчение - это просто проверить, является ли oldSeed таким же, прежде чем вызывать CAS по нему. Эта оптимизация (известная как тест-тест и набор или TTAS) может показаться избыточной, но может иметь важное влияние на производительность в условиях конкуренции, поскольку CAS не предпринимает попыток, если он уже знает, что потерпит неудачу. Неудачный CAS имеет нежелательный побочный эффект, заключающийся в установке состояния MESI строки кэша на Invalid, требуя, чтобы все стороны повторно извлекали значение из памяти. Конечно, разделение семян Хольгером - отличное долгосрочное решение, но даже в этом случае следует использовать оптимизацию TTAS.
Джед Уэсли-Смит
5
Вы имеете в виду «сотни тысяч» вместо «сотни или тысячи»? - БОЛЬШАЯ разница
Майкл Нил
30

Это похоже на "ошибку", которую можно обойти. Есть свойство, отключающее новую функцию «альтернативного хеширования»:

jdk.map.althashing.threshold = -1

Однако отключения альтернативного хеширования недостаточно, потому что он не отключает генерацию случайного начального числа хеширования (хотя это действительно должно быть). Таким образом, даже если вы отключите хеширование alt, у вас все равно будет конфликт потоков во время создания экземпляра хэш-карты.

Один особенно неприятный способ обойти это - принудительно заменить экземпляр, Randomиспользуемый для генерации начального числа хэша, вашей собственной несинхронизированной версией:

// Create an instance of "Random" having no thread synchronization.
Random alwaysOne = new Random() {
    @Override
    protected int next(int bits) {
        return 1;
    }
};

// Get a handle to the static final field sun.misc.Hashing.Holder.SEED_MAKER
Class<?> clazz = Class.forName("sun.misc.Hashing$Holder");
Field field = clazz.getDeclaredField("SEED_MAKER");
field.setAccessible(true);

// Convince Java the field is not final.
Field modifiers = Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);

// Set our custom instance of Random into the field.
field.set(null, alwaysOne);

Почему это (вероятно) безопасно? Поскольку альтернативное хеширование отключено, случайные начальные числа хеша игнорируются. Так что не имеет значения, что наш экземпляр Randomна самом деле не случайный. Как всегда с такими неприятными приемами, пожалуйста, используйте их с осторожностью.

(Спасибо https://stackoverflow.com/a/3301720/1899721 за код, который устанавливает статические конечные поля).

--- Редактировать ---

FWIW, следующее изменение HashMapустранит конфликт потоков при отключении альтернативного хеширования:

-   transient final int hashSeed = sun.misc.Hashing.randomHashSeed(this);
+   transient final int hashSeed;

...

         useAltHashing = sun.misc.VM.isBooted() &&
                 (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
+        hashSeed = useAltHashing ? sun.misc.Hashing.randomHashSeed(this) : 0;
         init();

Аналогичный подход можно использовать и для других ConcurrentHashMapцелей.

Cambecc
источник
1
Спасибо. Это действительно взлом, но он временно решает проблему. Это, безусловно, лучшее решение, чем любое из перечисленных выше. В долгосрочной перспективе мне все равно придется что-то делать с более быстрым HashMap. Это напоминает мне решение проблемы, когда старый кеш ResourceBundle не очищается. Код практически идентичен!
Stave Escura
1
К вашему сведению, эта функция альтернативного хеширования описана здесь: Запрос на проверку CR # 7118743: Альтернативное хеширование для строки с картами на основе хешей . Это реализация хеш-функции murmur3.
cambecc
3

Существует множество приложений, которые создают временную HashMap для каждой записи в приложениях с большими данными. Это парсеры и сериализаторы, например. Внесение любой синхронизации в классы несинхронизированных коллекций - настоящая проблема. На мой взгляд, это недопустимо и необходимо как можно скорее исправить. Изменение, которое, очевидно, было внесено в 7u6, CR # 7118743, должно быть отменено или исправлено без необходимости какой-либо синхронизации или атомарной операции.

Каким-то образом это напоминает мне о колоссальной ошибке синхронизации StringBuffer, Vector и HashTable в JDK 1.1 / 1.2. За эту ошибку люди дорого заплатили годами. Нет необходимости повторять этот опыт.

user1951832
источник
2

Предполагая, что ваш шаблон использования разумен, вы захотите использовать свою собственную версию Hashmap.

Этот фрагмент кода предназначен для того, чтобы затруднить возникновение хеш-коллизий, не позволяя злоумышленникам создавать проблемы с производительностью ( подробности ) - если эта проблема уже решена каким-то другим способом, я не думаю, что вам вообще понадобится синхронизация. Однако независимо от того, используете ли вы синхронизацию или нет, похоже, вы захотите использовать свою собственную версию Hashmap, чтобы не зависеть от того, что предоставляет JDK.

Так что либо вы обычно пишете что-то подобное и указываете на это, либо переопределяете класс в JDK. Чтобы сделать последнее, вы можете переопределить путь к классам начальной загрузки с помощью -Xbootclasspath/p:параметра. Однако это будет «противоречить лицензии на двоичный код Java 2 Runtime Environment» ( источник ).

eis
источник
Ага. Я не понимал, что в этом смысл оптимизации. Очень умный. Моя модель угроз для злоумышленников не предполагает, что они таким образом возятся с хэш-картами, но я запомню это на будущее. Я согласен с вашей точкой зрения о замене HashMap в конечном итоге. Мне, вероятно, придется встроить фабричный объект или, возможно, контейнер IOC в каждый класс, который их создает. Я думаю, что ответ Камбека вытащит меня из ямы, в то время как я работаю над более долгосрочным решением
Stave Escura