Мова програмування 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  описує взаємодію потоків через пам'ять в мові програмування Java. Найчастіше на сучасних комп'ютерах код заради швидкості виконується не в тому порядку, в якому він написаний. Перестановка виконується компілятором, процесором і підсистемою пам'яттю. Мова програмування Java не гарантує атомарність операцій і послідовну консистентність при читанні і записі полів розділюваних об'єктів. Дане рішення "розв'язує руки" компілятору і дозволяє проводити оптимізації (такі як розподілення регістрів, видалення загальних підвиразів і усунення зайвих операцій читання), основані на перестановці операцій доступу до пам'яті.

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

Комунікація потоків здійснюється за допомогою поділу доступу до полів і об'єктів, на які посилаються поля. Дана форма комунікації є дуже ефективною, але створює можливість виникнення помилок двох різновидів: втручання в потік (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 гарантує:

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

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

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

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

Історія 

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

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

Посилання

Посилання на зовнішні ресурси 
 Java Memory Model and Thread Specification simplified Chinese edition
 Sun/Oracle Java Concurrency tutorial
 Java Multithreading Steeplechase
 JDK 5.0 concurrency utilities
 Podcast from JavaPolis — 'Concurrency Utilities in JDK 5.0 by Brian Goetz'
 JavaOne 2005 — Concurrency Utilities in Practice
 JavaOne 2005 — Brian Goetz — Simpler, Faster, Better: Concurrency Utilities in JDK Software Version 5.0
 Podcast from JavaPosse — Interview with Brian Goetz on Concurrency in Java 5
 William Pugh's Java Memory Model page
 Java Concurrency Tutorial by Jakob Jenkov
 Thread Safe Java Programming by Vadym Ustymenko
 Java Concurrency Animations by Victor Grazi
 Thread safety checker for Java classes
 LiterateJava.com — Silent Thread death from unhandled exceptions
 Synchronization and Thread Safety in Java

Примітки 

Java
Паралельні обчислення
Sun Microsystems
Конкурентні обчислення
Статті з прикладами коду мовою Java