Мова програмування Java і JVM (Java Virtual Machine) розроблені з підтримкою паралельних обчислень, і всі обчислення виконуються в контексті потоку. Декілька потоків можуть спільно використовувати об'єкти і ресурси; кожен потік може виконувати свої інструкції (код), але потенційно може отримати доступ до будь-якого об'єкта в програмі. В обов'язки програміста входить координація (або «синхронізація») потоків під час операції запису і читання розділюваних об'єктів. Синхронізація потоків потрібна для того, щоб гарантувати, що одночасно до одного об'єкту може звертатися тільки один потік, і щоб запобігти звернення потоків до неповністю оновлених об'єктів в той час, як з ними працює інший потік. В мові Java є вбудовані конструкції підтримки синхронізації потоків.

Процеси і потоки

ред.

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

Об'єкти потоку

ред.

Потоки розділяють між собою ресурси процесора, а саме пам'ять і відкриті файли. Такий підхід веде до ефективної, але потенційно проблематичної, комунікації. Кожна програма має лише один потік, що виконується. Той потік з якого починає виконуватися програма, називається головним або основним. Головний потік здатний створювати додаткові потоки в вигляді об'єктів Runnable або Callable. (Інтерфейс Callable схожий на Runnable тим, що вони обоє розроблені для класів, екземпляри яких будуть виконуватися в окремому потоці. Але Runnable не повертає результату і не може викинути виняток, що перевіряється.)

Кожен потік може бути запланований для виконання на окремому ядрі ЦП, використовувати квантування часу на одноядерному процесорі або використовувати квантування часу на декількох процесорах. В двох останніх випадках система буде періодично переключатися між потоками, по черзі даючи виконуватися то одному, то іншому. Така схема називається псевдо-паралелізмом. Немає універсального рішення, яке сказало би як саме потоки Java будуть перетворені в нативні потоки ОС. Це залежить від конкретної реалізації JVM.

В мові Java потік представляється в вигляді об'єкту-потомка класу Thread. Цей клас інкапсулює стандартні механізми роботи з потоком. Потоками можна управляти або напряму, або засобами абстрактних механізмів, таких як Executor і колекції з пакету java.util.concurrent.

Запуск потоку

ред.

Запустити новий потік можна двома способами:

  • Реалізацією інтерфейсу Runnable
public class HelloRunnable implements Runnable {
    public void run() {
        System.out.println("Привіт з потоку!");
    }
    public static void main(String[] args) {
        (new Thread(new HelloRunnable())).start();
    }
}
  • Наслідуванням від класу Thread
public class HelloThread extends Thread {
    public void run() {
        System.out.println("Привіт з потоку!");
    }
    public static void main(String[] args) {
        (new HelloThread()).start();
    }
}

Переривання

ред.

Переривання — вказівка потоку, що він має припинити поточну роботу і зробити ще щось. Потік може послати переривання викликом методу interrupt() в об'єкта Thread, якщо потрібно перервати асоційований з ним потік. Механізм переривання реалізовано з використання внутрішньої мітки interrupt status (мітка переривання) класу Thread. Виклик Thread.interrupt() активує цю мітку. Згідно з узгодженнями будь-який метод, який завершується викиданням InterruptedException скидає мітку переривання. Перевірити чи встановлена та чи інша мітка можна двома способами. Перший спосіб — викликати метод bool isInterrupted() об'єкту потоку, другий — викликати статичний метод bool Thread.interrupted(). Перший метод повертає стан мітки переривання і залишає цю мітку недоторканою. Другий метод повертає стан мітки і скидає її. Зауважте, що Thread.interrupted() - статичний метод класу Thread, і його виклик повертає значення мітки переривання того потоку, з якого він був викликаний.

Очікування завершення

ред.

В Java передбачений механізм, який дозволяє одному потоку чекати виконання іншого. Для цього використовується метод Thread.join().

Демони

ред.

В Java процес завершується тоді, коли завершується останній його потік. Навіть коли метод main() вже завершився, але ще виконуються породжені ним потоки, система буде чекати їх завершення. Але це правило не відносить до особливого виду потоків — демонів. Якщо завершився останній звичайний потік процесу, і залишилися тільки потоки-демони, то вони будуть примусово завершені і виконання процесу закінчиться. Найчастіше потоки-демони використовуються для виконання фонових задач, які обслуговують процес протягом його життя.

Оголосити потік демоном досить просто - потрібно перед запуском потоку викликати його метод setDaemon(true); перевірити, чи є потік демоном, можна викликавши його метод boolean isDaemon().

Винятки

ред.

Викинутий і необроблений виняток веде до завершення потоку. Головний потік автоматично виведе виняток в консоль, а потоки, створені користувачем, можуть зробити це тільки зареєструвавши оброблювач.

Модель пам'яті

ред.

Модель пам'яті Java [1] описує взаємодію потоків через пам'ять в мові програмування Java. Найчастіше на сучасних комп'ютерах код заради швидкості виконується не в тому порядку, в якому він написаний. Перестановка виконується компілятором, процесором і підсистемою пам'яттю. Мова програмування Java не гарантує атомарність операцій і послідовну консистентність при читанні і записі полів розділюваних об'єктів. Дане рішення “розв'язує руки” компілятору і дозволяє проводити оптимізації (такі як розподілення регістрів, видалення загальних підвиразів і усунення зайвих операцій читання), основані на перестановці операцій доступу до пам'яті.[1]

Синхронізація

ред.

Комунікація потоків здійснюється за допомогою поділу доступу до полів і об'єктів, на які посилаються поля. Дана форма комунікації є дуже ефективною, але створює можливість виникнення помилок двох різновидів: втручання в потік (thread interference) і помилки консистентності пам'яті (memory consistency errors). Для запобігання їх виникнення існує механізм синхронізації.

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

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

Синхронізація має кілька аспектів. Найбільш добре розуміється взаємне виключення (mutual exclusion) - тільки один потік може володіти монітором, таким чином синхронізація на моніторі означає, що як тільки один потік входить в synchronized-блок, захищений монітором, ніякий інший потік не може увійти в блок, захищений цим монітором поки перший потік не вийде з synchronized-блоку.

Але синхронізація - це більше ніж просто взаємне виключення. Синхронізація гарантує, що дані, записані в пам'ять до або всередині синхронізованого блоку, стають видимими для інших потоків, які синхронізуються на тому ж моніторі. Після того як ми виходимо з синхронізованого блоку, ми звільняємо (release) монітор, що має ефект скидання (flush) кешу в оперативну пам'ять, так що записи, зроблені нашим потоком, можуть бути видимими для інших потоків. Перш ніж ми зможемо увійти в синхронізований блок, ми захоплюємо (acquire) монітор, що має ефект оголошення недійсними даних локального процесорного кешу (invalidating the local processor cache), так що змінні будуть завантажені з основної пам'яті. Тоді ми зможемо побачити всі записи, зроблені видимими попереднім звільненням (release) монітора. (JSR 133)

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

Блокування і synchonized-блоки

ред.

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

Volatile поля

ред.

Стосовно до полів ключове слово volatile гарантує:

  1. (У всіх версіях Java) Доступи до volatile-змінної впорядковані глобально. Це означає, що кожен потік, який звертається до volatile-полю, прочитає його значення перед тим як продовжити замість того, щоб (по можливості) використовувати закешоване значення. (Доступи до volatile-змінної не можуть бути переупорядкувані один з одним, але вони можуть бути переупорядкувані з доступами до звичайних змінних. Це зводить нанівець корисність volatile-полів як засобу передачі сигналу від одного потоку до іншого.)
  2. (В Java 5 і пізніших) Запис в volatile-поле має той же ефект для пам'яті, що і звільнення монітора (англ. Monitor release), а читання - той же, що і захоплення (англ. Monitor acquire). Доступ до volatile-полів встановлює відношення «Виконується перед» (англ. happens before).[2] По суті, це відношення є гарантією того, що все, що було очевидно для потоку A, коли він писав в volatile-поле f, стає видимим для потоку B, коли він прочитає f.

Volatile-поля є атомарними. Читання з volatile-поля має той же ефект, що і отримання блокування: дані в робочій пам'яті оголошуються недійсними, значення volatile-поля заново читається з пам'яті. Запис в volatile-поле має той же ефект для пам'яті, що і звільнення блокування: volatile-поле негайно записується в пам'ять.

Фінальні поля

ред.

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

Історія

ред.

Починаючи з JDK 1.2, в Java включений стандартний набір класів-колекцій Java Collections Framework.

Даг Лі, який також брав участь в реалізації Java Collections Framework, розробив пакет concurrency, що включає в себе кілька примітивів синхронізації і велику кількість класів, що відносяться до колекцій.[3] Робота над ним була продовжена як частина JSR 166[4] під головуванням Дага Лі. Реліз JDK 5.0 включив багато доповнень і пояснень до моделі паралелізму в Java. Вперше API для роботи з паралелізмом розроблені JSR 166 були включені в JDK. JSR 133 надала підтримку для добре визначених атомарних операцій в багатопотоковому/багатопроцесорному оточенні. І Java SE 6, і Java SE 7 привносять зміни та доповнення в JSR 166 API.

Посилання

ред.
  • Goetz, Brian; Joshua Bloch; Joseph Bowbeer; Doug Lea; David Holmes; Tim Peierls (2006). Java Concurrency in Practice. Addison Wesley. ISBN 0-321-34960-1.
  • Lea, Doug (1999). Concurrent Programming in Java: Design Principles and Patterns. Addison Wesley. ISBN 0-201-31009-0.

Посилання на зовнішні ресурси

ред.

Примітки

ред.
  1. Herlihy, Maurice, and Nir Shavit. «The art of multiprocessor programming.» PODC. Vol. 6. 2006.
  2. Section 17.4.4: Synchronization Order |title=The Java® Language Specification, Java SE 7 Edition . Oracle Corporation. 2013 http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4.4. Процитовано 12 травня 2013. {{cite web}}: Пропущений або порожній |title= (довідка)
  3. Даг Ли. Overview of package util.concurrent Release 1.3.4. Процитовано 1 січня 2011. Note: Upon release of J2SE 5.0, this package enters maintenance mode: Only essential corrections will be released. J2SE5 package java.util.concurrent includes improved, more efficient, standardized versions of the main components in this package.
  4. JSR 166: Concurrency Utilities. Архів оригіналу за 3 листопада 2016. Процитовано 3 листопада 2016.