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

Недавно я задал вопрос в 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 показывает, что большинство потоков тратят большую часть своего времени на вращение в этом цикле в Random.

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

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

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

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

Ответ 1

Я являюсь оригинальным автором патча, который появился в 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.

Ответ 2

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

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 на самом деле не случайен. Как всегда с такими неприятными хаками, пожалуйста, используйте с осторожностью.

(Благодаря fooobar.com/questions/18683/... для кода, который устанавливает статические конечные поля).

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

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 и т.д.

Ответ 3

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

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

Ответ 4

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

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

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