Теорія і практика Java: Оптимізації синхронізацій в Mustang

  1. Серія контенту:
  2. Цей контент є частиною серії: Теорія і практика Java
  3. пропуск блокування
  4. Малюнок 1. Синхронізація і видимість в Java Memory Model
  5. Лістинг 1. Використання локального поточного об'єкта в якості блокування
  6. Лістинг 2. Кандидат на пропуск блокування
  7. адаптивна блокування
  8. укрупнення блокування
  9. Лістинг 3. Кандидат на укрупнення блокування
  10. висновок
  11. Ресурси для скачування

Теорія і практика Java

Escape-аналіз може допомогти оптимізувати синхронізацію

Серія контенту:

Цей контент є частиною # з серії # статей: Теорія і практика Java

https://www.ibm.com/developerworks/ru/views/global/libraryview.jsp?series_title_by=Теория+и+практика+java

Слідкуйте за виходом нових статей цієї серії.

Цей контент є частиною серії: Теорія і практика Java

Слідкуйте за виходом нових статей цієї серії.

У всіх випадках, коли змінюються змінні доступні для всіх потоків, вам необхідно використовувати синхронізацію, щоб переконатися в тому, що вироблені одним потоком поновлення своєчасно видимі для інших потоків. Основні засоби синхронізації - це використання synchronized-блоків, які надають взаємовиключення і гарантують видимість. (Інші форми синхронізації включають змінні (volatile) змінні, об'єкти Lock (об'єкти блокування) в java.util.concurrent.locks і атомарні змінні. Коли два потоку хочуть отримати доступ до змінюваної змінної загального користування, вони не тільки повинні використовувати синхронізацію, але якщо вони використовують synchronized-блоки, ці synchronized-блоки повинні використовувати той же lock-об'єкт.

На практиці блокування (locking) ділиться на дві категорії: в основному змагальну (contended) і в основному несостязательную (uncontended). Contended-блокування є "гарячими" блокуваннями в додатку, такими як блокування, які захищають спільну робочу чергу від пулу потоку. Багатьом потокам постійно потрібні дані, захищені цими блокуваннями, і ви можете очікувати, що при запиті такого блокування вам доведеться почекати, поки один з потоків не звільнить її. Uncontended-блокування - це ті блокування, які захищають дані, до яких не так часто звертаються, тому більшу частину часу, коли потік отримує блокування, жоден з інших потоків не може утримувати цю блокування. Більшість блокувань нечасто є змагальними, таким чином, підвищення продуктивності uncontended-блокування може істотно підвищити загальну продуктивність при виконанні програми.

JVM має окремі шляхи коду для отримання contended ( "повільний шлях") і uncontended ( "швидкий шлях") захоплень блокування. Було витрачено багато зусиль на оптимізацію швидкого шляху, тоді як Mustang покращує і швидкий шлях, і повільний, а також додає кілька оптимізацій, що може повністю усунути деякі з блокувань.

пропуск блокування

Java Memory Model говорить про те, що потік, який виходить з synchronized-блоку, відбувається до того, як інший потік входить в synchronized-блок, захищений тієї ж блокуванням. Це означає, що будь-які операції пам'яті не були видимі потоку А, коли він виходить з synchronized-блоку, захищеного блокуванням М, вони будуть видні потоку В, коли той входить в synchronized-блок, захищений M, як показано на рисунку 1. Для synchronized-блоків, які використовують різні блокування, ми не можемо нічого припускати про їх розташування, як ніби зовсім не було ніякої синхронізації.

Малюнок 1. Синхронізація і видимість в Java Memory Model
Теорія і практика Java   Escape-аналіз може допомогти оптимізувати синхронізацію   Серія контенту:   Цей контент є частиною # з серії # статей: Теорія і практика Java   https://www

Очевидно, що якщо потік потім входить в synchronized-блок, захищений блокуванням, який жоден з потоків не буде синхронізувати, то ця синхронізація не має ніякого ефекту і, таким чином, може бути видалена оптимізатором. (Java Language Specification дозволяє явно таку оптимізацію.) Такий сценарій може здатися неймовірним, але бувають випадки, коли це ясно компілятору. Лістинг 1 показує спрощений приклад локального поточного (thread-local) lock-об'єкта:

Лістинг 1. Використання локального поточного об'єкта в якості блокування

synchronized (new Object ()) {doSomething (); }

Так як посилання на lock-об'єкт зникає перед тим, як будь-який інший потік може його використовувати, компілятор може визначити, що попередня синхронізація може бути видалена, так як два потоки не можуть синхронізувати одну і ту ж блокування. Так як ніхто не буде безпосередньо використовувати рішення з лістингу 1, даний код дуже схожий на випадок, коли блокування, пов'язану з synchronized-блоком, можна підтвердити як локальної потокової змінної (thread-local variable). "Thread-local" не обов'язково означає, що вона реалізується класом ThreadLocal. Вона може бути будь-якої змінної, до якої, як може зрозуміти компілятор, не має доступу ні один з інших потоків. Об'єкти, на які посилаються локальні змінні і які ніколи не виходять із заданих їм кордонів, проходять цей тест - якщо об'єкт укладений в стек будь-якого з потоків, жоден з потоків не зможе побачити посилання на цей об'єкт. (Єдиний спосіб, за допомогою якого можна відкрити загальний доступ до об'єктів різних потоків - це коли посилання на нього розташована в динамічної пам'яті.)

На щастя чи на нещастя, escape-аналіз, який ми обговорювали в минулого місяця , Надає компілятор з точною інформацією, яка необхідна для оптимізації synchronized-блоків, які використовують локальні потокові lock-об'єкти. Якщо компілятор може розпізнати (використовуючи escape-аналіз), що об'єкт ніколи не розташовувався в динамічної пам'яті, то це повинен бути локальний поточний об'єкт і більш того будь-які synchronized-блоки, які використовують цей об'єкт як блокування, ніяк не впливатимуть на Java Memory Model (JMM) і можуть бути усунені. Така оптимізація називається lock elision (пропуск блокування) і є ще одним способом JVM-оптимізації, впровадженим в Mustang.

Використання блокування за допомогою локального поточного об'єкта застосовується набагато частіше, ніж ви можете думати. Існує багато класів, таких як StringBuffer і java.util.Random, які захищені від потоку (thread-safe), тому що можуть використовуватися відразу великою кількістю потоків, але їх часто використовують як thread-local.

Розглянемо код в лістингу 2, де використовується Vector для створення строкового значення. Метод getStoogeNames () створює Vector, додає до нього кілька рядків, а потім викликає toString () для конвертації його в рядок. Кожен з викликів одного з методів Vector - три виклики add () і один toString () - вимагає запиту і звільнення блокування Vector. У той час, як всі запити на блокування будуть uncontended, і, отже, швидкими, компілятор може повністю усунути синхронізацію, використовуючи пропуск блокування.

Лістинг 2. Кандидат на пропуск блокування

public String getStoogeNames () {Vector v = new Vector (); v.add ( "Moe"); v.add ( "Larry"); v.add ( "Curly"); return v.toString (); }

Так як ні одна посилання на Vector не пропускає метод getStoogeNames (), то необхідний thread-local і, отже, будь-який synchronized-блок, який використовує його як блокування, що не буде ніяк впливати на JMM. Компілятор може підключати виклики методів add () і toString (), а потім він розпізнає, що він запитує і звільняє блокування на об'єкт thread-local і може оптимізувати всі чотири операції блокування і розблокування заблокованих функцій.

Як вже було сказано вище, спроба уникнути синхронізації, в общем-то, не найкраща ідея. Надходження оптимізацій, таких як lock elision adds (додавання пропуску блокування), не мають більше підстав для усунення синхронізації - компілятор може виконувати її автоматично, коли це є безпечним, або залишити її в зворотному випадку.

адаптивна блокування

На додаток до escape-аналізу і пропуску блокування Mustang також має й інші оптимізації для виконання блокування. Коли два потоку претендують на блокування, один з них отримає блокування, а іншому доведеться блокуватися доти, поки блокування не звільнитися. Існує дві наочних методики для реалізації блокування (blocking): операційна система припиняє потік до тих пір, поки йому не потрібно активуватися або використання spin locks (взаімоблокіровок). Взаімоблокіровка, в основному, зводиться до наступного коду:

while (lockStillInUse);

У той час як взаимоблокировки активно завантажують центральний процесор і виявляються неефективними, вони можуть стати більш ефективними, ніж припинення потоку і подальша його активація, якщо розглянута блокування утримується дуже мала кількість часу. При цьому виникають значні витрати, коли потік припиняється і відбувається його перепланування, що залучає до роботи JVM, операційну систему і апаратне забезпечення. Це є проблемою для тих, хто працює з JVM: якщо блокування утримуються тільки на короткий проміжок часу, тоді взаімоблокіровка більш ефективна. Якщо ж блокування утримуються на довгий проміжок часу, то найбільш ефективно утримування. Без інформації про те, для чого потрібно розподіл часу блокувань для цього додатка, велика частина віртуальних машин JVM поводиться консервативно і просто утримує потік, коли йому не вдається отримати блокування.

Однак це можна зробити ще краще. Для кожної блокування JVM може адаптивно вибирати між взаімоблокіровка (spinning) і утримуванням (suspension), на основі поведінки минулих запитів. Вона може спробувати взаємоблокування і, якщо це спрацювало після певного проміжку часу, вона продовжує слідувати цьому вибору. Якщо ж, з іншого боку, певна кількість взаімоблокіровок не підтверджують відповідний результат блокування, вона може вирішити, що це блокування утримується на "великий проміжок часу" і перекомпілюються метод, щоб використовувати тільки утримування. Це рішення може бути зроблено на основі "per-lock" або "per-lock-site".

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

укрупнення блокування

Інша оптимізація, яку можна використовувати для зниження споживання ресурсів блокуванням є lock coarsening (укрупнення блокування). Укрупнення блокування - це процес, коли з'єднуються суміжні synchronized-блоки, які використовують один і той же lock-об'єкт. Якщо компілятор не може усунути блокування за допомогою пропуску блокування, то він може знизити надлишки споживання ресурсів за допомогою укрупнення блокування.

Код, наведений в лістингу 3, не є обов'язковим кандидатом на пропуск блокування (хоча після вбудовування addStoogeNames (), віртуальна машина JVM все ще може ігнорувати блокування), але зате дійсно може отримати вигоду від укрупнення блокування. Три виклику add () по черзі захоплюють блокування Vector, щось роблять і звільняють блокування. Компілятор може виявити, що є послідовність суміжних блоків, які оперують з одним блокуванням, а потім з'єднати їх в один блок.

Лістинг 3. Кандидат на укрупнення блокування

public void addStooges (Vector v) {v.add ( "Moe"); v.add ( "Larry"); v.add ( "Curly"); }

Мало того, що це знижує зайве споживання ресурсів блокування, але за допомогою об'єднання блоків три виклики методів об'єднуються в один великий synchronized-блок, що дає оптимізатора набагато більший базовий блок, з яким потрібно працювати. Таким чином, укрупнення блокування може активувати інші оптимізації, які не мають нічого спільного з блокуванням.

Навіть якщо є інші оператори між synchronized-блоками або викликами методів, компілятор все ще може виробляти укрупнення блокування. Компілятору дозволяється прибирати оператори в synchronized-блок - але не з нього. Таким чином, якщо такі оператори мали місце між викликами add (), компілятор міг об'єднати всі addStooges () в один великий synchronized-блок, де Vector використовується, як lock-об'єкт.

Укрупнення блокування може вплинути на досягнення компромісу між продуктивністю і швидкістю реакції. За допомогою об'єднання пар lock-unlock в єдину пару lock-unlock, відбудеться збільшення продуктивності завдяки зниженню підрахунку інструкцій і зниження обсягу трафіку синхронізації на шині пам'яті. Ціною є те, що період часу, на яке затримується блокування, може збільшуватися, підвищуючи час, на яке інші потоки можуть затримувати блокування, а також підвищуючи ймовірність стану змагання за блокування (lock contention). Однак в будь-якому випадку блокування затримується на досить короткий проміжок часу і компілятор може застосувати евристичні процедури, на основі довжини коду, захищеного синхронізацією, для створення розумного компромісу. (І в залежності від того, які інші оптимізації активовані великим основним блоком, тривалість, на яку затримується блокування, не може більше бути збільшена при укрупненні.)

висновок

Продуктивність несоревновательной блокування збільшилася майже у всіх версіях JVM. Mustang продовжує цю тенденцію і підвищує безпосередню продуктивність змагальної і несоревновательной блокування, а також представляє оптимізації, які можуть усунути багато операцій блокування.

Ресурси для скачування

Схожі теми

Підпишіть мене на повідомлення до коментарів

Jsp?