Недавно я поднял вопрос в 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
Прежде чем я начну использовать любой из этих путей (все выглядят очень трудоемкими и потенциально важными), я подумал, не упустил ли я очевидный трюк. Может ли кто-нибудь из вас подсказать, какой путь лучше, или, возможно, определить новую идею.
Спасибо за помощь
источник
AtomicLong
делает ставку на низкий уровень конкуренции за запись, чтобы работать хорошо. У вас высокая конкуренция за запись, поэтому вам нужна регулярная монопольная блокировка. Напишите синхронизированнуюHashMap
фабрику, и вы, вероятно, увидите улучшение, если все, что вы когда-либо делаете в этих потоках, - это создание экземпляров карты.Ответы:
Я являюсь первоначальным автором патча, который появился в 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 увидят его.
источник
Это похоже на "ошибку", которую можно обойти. Есть свойство, отключающее новую функцию «альтернативного хеширования»:
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
целей.источник
Существует множество приложений, которые создают временную HashMap для каждой записи в приложениях с большими данными. Это парсеры и сериализаторы, например. Внесение любой синхронизации в классы несинхронизированных коллекций - настоящая проблема. На мой взгляд, это недопустимо и необходимо как можно скорее исправить. Изменение, которое, очевидно, было внесено в 7u6, CR # 7118743, должно быть отменено или исправлено без необходимости какой-либо синхронизации или атомарной операции.
Каким-то образом это напоминает мне о колоссальной ошибке синхронизации StringBuffer, Vector и HashTable в JDK 1.1 / 1.2. За эту ошибку люди дорого заплатили годами. Нет необходимости повторять этот опыт.
источник
Предполагая, что ваш шаблон использования разумен, вы захотите использовать свою собственную версию Hashmap.
Этот фрагмент кода предназначен для того, чтобы затруднить возникновение хеш-коллизий, не позволяя злоумышленникам создавать проблемы с производительностью ( подробности ) - если эта проблема уже решена каким-то другим способом, я не думаю, что вам вообще понадобится синхронизация. Однако независимо от того, используете ли вы синхронизацию или нет, похоже, вы захотите использовать свою собственную версию Hashmap, чтобы не зависеть от того, что предоставляет JDK.
Так что либо вы обычно пишете что-то подобное и указываете на это, либо переопределяете класс в JDK. Чтобы сделать последнее, вы можете переопределить путь к классам начальной загрузки с помощью
-Xbootclasspath/p:
параметра. Однако это будет «противоречить лицензии на двоичный код Java 2 Runtime Environment» ( источник ).источник