1. Апаратна архітектура пам'яті

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

Ось спрощена схема апаратної архітектури сучасного комп'ютера:

Апаратна архітектура пам'яті

У сучасному світі комп'ютер має два або більше процесорів, і це вже норма. Деякі з цих процесорів можуть мати кілька ядер. На таких комп'ютерах можливе одночасне виконання кількох потоків. Кожне ядро процесора здатне виконувати один потік у будь-який час. Це означає, що будь-яка Java-програма є апріорі багатопоточною, і всередині твоєї програми можна запустити по одному потоку на ядро процесора одночасно.

Ядро процесора містить набір регістрів, які знаходяться в його пам'яті (всередині ядра). Воно виконує операції над даними регістру набагато швидше, ніж над даними, які знаходяться в основній пам'яті комп'ютера (ОЗУ). Це пов'язано з тим, що процесор може отримати доступ до цих регістрів набагато швидше.

Кожен ЦП також може мати свій шар кеш-пам'яті. Більшість сучасних процесорів його мають. Процесор може отримати доступ до своєї кеш-пам'яті набагато швидше, ніж до основної пам'яті, але не так швидко, як до власних внутрішніх регістрів. Значення швидкості доступу до кеш-пам'яті – приблизно між швидкостями доступу до основної пам'яті та внутрішніх регістрів.

До того ж, у процесорів є багаторівневий кеш. Але це не дуже важливо знати для розуміння того, як Java модель пам'яті взаємодіє з апаратною пам'яттю. Важливо знати, що процесори можуть мати певний рівень кеш-пам'яті.

Будь-який комп'ютер також містить ОЗП (область основної пам'яті). Усі ядра можуть отримати доступ до основної пам'яті. Основна область пам'яті зазвичай набагато більша, ніж кеш-пам'ять ядер процесорів.

У момент, коли процесору потрібен доступ до основної пам'яті, він зчитує її частину до своєї кеш-пам'яті. Він може також зчитувати частину даних з кешу до своїх внутрішніх регістрив і виконувати операції над ними. Коли ЦПУ необхідно буде записати результат знову в основну пам'ять, він скине дані зі свого внутрішнього регістру до кеш-пам'яті, а в якийсь момент – до основої пам'яті.

Дані, що зберігаються в кеш-пам'яті, зазвичай скидаються назад до основної пам'яті, коли процесору необхідно зберегти в кеш-пам'яті щось ще. Кеш може одночасно очищати свою пам'ять і записувати дані. У процесора нема необхідності читати або записувати повний кеш щоразу під час оновлення. Зазвичай кеш оновлюється невеликими блоками пам'яті: вони називаються рядком кешу. Один або кілька рядків кешу можуть бути зчитані до кеш-пам'яті, і один або більше рядків кешу можуть бути скинуті назад до основної пам'яті.

2. Суміщення Java-моделі пам'яті та апаратної архітектури пам'яті

Як вже згадувалося, Java-модель пам'яті та апаратна архітектура пам'яті різні. Апаратна архітектура не розрізняє стеки потоків та купу. На обладнанні стек потоків та HEAP (купа) знаходяться в основній пам'яті.

Частини стеків і купи потоків можуть бути присутніми в кешах і внутрішніх регістрах ЦП. Це показано на діаграмі:

стек потоків та HEAP

Якщо об'єкти та змінні можуть зберігатися в різних сферах пам'яті комп'ютера, можуть виникнути певні проблеми. Ось дві основні:

  • видимість змін, які потік зробив над загальними змінними;
  • стан гонки під час читання, перевірки та запису загальних змінних.

Обидві ці проблеми поясню далі.

3. Видимість загальних об'єктів

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

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

Наступна діаграма ілюструє малюнок цієї ситуації. Один потік, що працює на лівому ЦП, копіює до його кеша загальний об'єкт і змінює значення змінної count на 2. Ця зміна невидима для інших потоків, що працюють на правому ЦП, оскільки оновлення для count ще не скинуто назад до основної пам'яті.

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

4. Стан гонки (Race Condition)

Якщо два або більше потоків спільно використовують один об'єкт і більше одного потоку оновлюють змінні в цьому загальному об'єкті, може виникнути стан гонки.

Уяви, що потік A зчитує змінну count загального об'єкта до кеша свого процесора. Уяви також, що потік B робить те саме, але до кеша іншого процесора. Тепер потік A додає 1 до значення змінної count, і потік B робить те саме. Тепер змінну було збільшено двічі — окремо по +1 у кеші кожного процесора.

Якби ці збільшення були виконані послідовно, змінна count була б збільшена вдвічі, і назад до основної пам'яті було б записано (вихідне значення +2).

Тим не менш, два збільшення були виконані одночасно без належної синхронізації. Незалежно від того, який з потоків (A або B) записує свою оновлену версію count до основної пам'яті, нове значення буде тільки на 1 більше вихідного значення, незважаючи на два збільшення.

Ця діаграма ілюструє виникнення проблеми зі станом гонки, що описано вище:

Для вирішення цієї проблеми можна використовувати синхронізований блок Java. СВін гарантує, що лише один потік може увійти до критичного розділу коду в будь-який момент.

Синхронізовані блоки також гарантують, що всі змінні, до яких звертаються всередині синхронізованого блоку, будуть зчитані з основної пам'яті, і коли потік вийде з синхронізованого блоку, всі оновлені змінні будуть знову скинуті до основної пам'яті, незалежно від того, оголошена змінна як volatile чи ні.