Враховуючи, що HashMaps у jdk1.6 та вище викликають проблеми з multi = потоками, як мені виправити свій код


83

Нещодавно я підняв питання в stackoverflow, а потім знайшов відповідь. Початкове питання полягало в тому, які механізми, крім mutexx або збору сміття, можуть уповільнити мою багатопотокову програму 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

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

Дякую за допомогу


2
Що вимагає від вас такої кількості хеш-карт? Що ти намагаєшся робити?
fge

3
2 коментарі: 1. ConcurrentHashMap, здається, не використовує цього - чи може це бути альтернативою? 2. Цей фрагмент коду викликається лише при створенні карти. Це означає, що ви створюєте мільйони хеш-карт із суперечками - чи справді це відображає реальне виробниче навантаження?
assylias

1
Насправді ConcurrentHashMap використовує і цей метод (в oracle jdk 1.7_10) - але, мабуть, openJDK 7 цього не робить .
assylias

1
@assylias Вам слід перевірити останню версію тут . Цей займається таким рядком коду.
Marko Topolnik

3
@StaveEscura AtomicLongробить ставку на низьку конкуренцію, щоб добре працювати. У вас високий рівень переписки, тому вам потрібно регулярне ексклюзивне блокування. Напишіть синхронізовану HashMapфабрику, і ви, мабуть, побачите покращення, якщо тільки все, що ви коли-небудь робите в цих потоках, не є створенням карти.
Marko Topolnik

Відповіді:


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 людей можуть натиснути його секунду, а це означає, що мільйони карт можна створити дуже швидко. Звичайно, я можу переписати його, щоб не використовувати карти, але це коштує дуже великих витрат на розробку. Наразі план використання відображення для злому Випадкового поля виглядає непогано
Стейв Ескура

2
Майк, пропозиція щодо виправлення на короткий термін: окрім ThreadLocalRandom (який матиме свої проблеми з програмами, які псуються з локальним сховищем), чи не буде набагато простіше та дешевше (з точки зору часу, ризику та тестування) смужку Hashing.Holder.SEED_MAKER у масив (скажімо) <число ядер> Випадкових екземплярів і використовувати ідентифікатор виклику потоку для% -index? Це повинно негайно полегшити (хоча і не усунути) суперечку між потоками без помітних побічних ефектів.
Holger Hoffstätte

10
Веб-програми @mduigou, які мають високий рівень запитів і використовують JSON, збираються створити велику кількість HashMaps в секунду, оскільки більшість, якщо не всі бібліотеки JSON, використовують HashMaps або LinkedHashMaps для десериалізації об'єктів JSON. Веб-додатки, які використовують JSON, широко поширені, і створення HashMaps може контролюватися не додатком (а використанням бібліотечних додатків), тому я б сказав, що є вагомі причини не мати вузького місця під час створення HashMaps.
sbordet

3
@mduigou, можливо, простим полегшенням є просто перевірити, чи oldSeed однаковий, перш ніж викликати CAS на ньому. Ця оптимізація (відома як тест-тест і встановлення або TTAS) може здатися зайвою, але може мати суттєвий вплив на продуктивність, оскільки суперечка не робиться, якщо CAS вже не знає, що не вдасться. Помилка CAS має побічний побічний ефект встановлення статусу MESI рядка кешу на Недійсний - вимагає від усіх сторін повторного отримання значення з пам'яті. Звичайно, очищення насіння Хольгером є чудовим довгостроковим виправленням, але навіть тоді слід використовувати оптимізацію 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);

Чому це (мабуть) безпечно робити? Оскільки хешування alt вимкнено, через що випадкові насіння хешу ігноруються. Тож не має значення, що наш екземпляр Randomнасправді не є випадковим. Як завжди з такими неприємними хаками, будь ласка, використовуйте з обережністю.

(Дякуємо https://stackoverflow.com/a/3301720/1899721 за код, який встановлює статичні кінцеві поля).

--- Редагувати ---

FWIW, наступна зміна, щоб HashMapусунути суперечку потоку, коли хешування alt вимкнено:

-   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тощо.


1
Дякую. Це справді хакерство, але тимчасово вирішує проблему. Це, безумовно, краще рішення, ніж будь-яке зі списку, який я вказав вище. Довгостроково мені все одно доведеться щось робити з більш швидкою HashMap. Це нагадує мені рішення старого кешу ResourceBundle, який не можна очистити. Код майже ідентичний!
Стейв Ескура


3

Існує безліч програм, які створюють перехідну HashMap на запис у додатках для великих даних. Це парсери та серіалізатори, наприклад. Введення будь-якої синхронізації в несинхронізовані класи колекцій - справжня помилка. На мою думку, це неприпустимо і потребує виправлення якомога швидше. Зміна, яка, очевидно, була введена в 7u6, CR # 7118743, повинна бути скасована або виправлена, не вимагаючи жодної синхронізації або атомної операції.

Це якимось чином нагадує мені колосальну помилку синхронізації StringBuffer та Vector та HashTable у JDK 1.1 / 1.2. Люди роками дорого платили за цю помилку. Не потрібно повторювати цей досвід.


2

Припускаючи, що ваш спосіб використання є розумним, ви захочете використовувати власну версію Hashmap.

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

Отже, ви просто пишете щось подібне і вказуєте на це, або замінюєте клас у JDK. Щоб зробити останнє, ви можете перевизначити шлях завантаження класу -Xbootclasspath/p:параметром. Однак це "суперечить ліцензії двійкового коду середовища виконання Java 2" ( джерело ).


Ага. Я не усвідомлював, що в цьому полягає сенс оптимізації. Дуже розумний. Моя модель загрози для зловмисників не дозволяє їм возитися з хеш-картами таким чином, але я запам’ятаю це на майбутнє. Я погоджуюсь з Вашим твердженням щодо остаточної заміни HashMap. Ймовірно, мені доведеться вводити заводський об'єкт або, можливо, контейнер IOC до кожного класу, який їх робить. Я думаю, що відповідь Камбека виведе мене з діри, поки я працюю над довгостроковим рішенням
Стейв Ескура
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.