Параллелизм в JavaЯзык программирования Java и JVM (Java Virtual Machine) разработаны с поддержкой параллельных вычислений, и все вычисления выполняются в контексте потока. Несколько потоков могут совместно использовать объекты и ресурсы; каждый поток выполняет свои инструкции (код), но потенциально может получить доступ к любому объекту в программе. В обязанности программиста входит координация (или «синхронизация») потоков во время операций чтения и записи разделяемых объектов. Синхронизация потоков нужна для того, чтобы гарантировать, что одновременно к объекту может обращаться только один поток, и чтобы предотвратить обращение потоков к неполностью обновленным объектам в то время, как с ними работает другой поток. В языке Java есть встроенные конструкции поддержки синхронизации потоков. Процессы и потокиБольшинство реализаций виртуальной машины Java используют единственный процесс для выполнения программы и в языке программирования Java понятие параллельные вычисления чаще всего связывают с потоками. Потоки иногда называют лёгкими процессами. Объекты потокаПотоки разделяют между собой ресурсы процесса, в частности память и открытые файлы. Такой подход ведёт к эффективной, но потенциально проблематичной коммуникации. Каждое приложение имеет хотя бы один выполняющийся поток. Тот поток, с которого начинается выполнение программы, называется главным или основным. Главный поток способен создавать дополнительные потоки в виде объектов Каждый поток может быть запланирован для выполнения на отдельном ядре ЦП, использовать квантование времени на одноядерном процессоре или использовать квантование времени на нескольких процессорах. В последних двух случаях система будет периодически переключаться между потоками, поочередно давая выполняться то одному, то другому потоку. Такая схема называется псевдо-параллелизмом. Нет универсального решения, которое сказало бы как именно потоки Java будут преобразованы в нативные потоки ОС. Это зависит от конкретной реализации JVM. В языке Java поток представляется в виде объекта-потомка класса Запуск потокаЗапустить новый поток можно двумя способами:
public class HelloRunnable implements Runnable {
public void run() {
System.out.println("Привет из потока!");
}
public static void main(String[] args) {
(new Thread(new HelloRunnable())).start();
}
}
public class HelloThread extends Thread {
public void run() {
System.out.println("Привет из потока!");
}
public static void main(String[] args) {
(new HelloThread()).start();
}
}
ПрерыванияПрерывание — указание потоку, что он должен прекратить текущую работу и сделать что-то ещё. Поток может послать прерывание вызовом метода interrupt() у объекта Ожидание завершенияВ Java предусмотрен механизм, позволяющий одному потоку ждать завершения выполнения другого. Для этого используется метод Thread.join(). ДемоныВ Java процесс завершается тогда, когда завершается последний его поток. Даже если метод main() уже завершился, но ещё выполняются порождённые им потоки, система будет ждать их завершения. Однако это правило не относится к особому виду потоков — демонам. Если завершился последний обычный поток процесса, и остались только потоки-демоны, то они будут принудительно завершены и выполнение процесса закончится. Чаще всего потоки-демоны используются для выполнения фоновых задач, обслуживающих процесс в течение его жизни. Объявить поток демоном достаточно просто — нужно перед запуском потока вызвать его метод setDaemon(true); проверить, является ли поток демоном, можно вызвав его метод boolean isDaemon(). ИсключенияВыброшенное и необработанное исключение приведёт к завершению потока. Главный поток автоматически напечатает исключение в консоль, а потоки, созданные пользователем, могут сделать это только зарегистрировав обработчик.[1][2] Модель памятиМодель памяти Java [1] описывает взаимодействие потоков через память в языке программирования Java. Зачастую на современных компьютерах код ради скорости выполняется не в том порядке, в котором написан. Перестановка выполняется компилятором, процессором и подсистемой памяти. Язык программирования Java не гарантирует атомарность операций и последовательную консистентность при чтении или записи полей разделяемых объектов. Данное решение «развязывает руки» компилятору и позволяет проводить оптимизации (такие как распределение регистров, удаление общих подвыражений и устранение лишних операций чтения), основанные на перестановке операций доступа к памяти.[3] СинхронизацияКоммуникация потоков осуществляется посредством разделения доступа к полям и объектам, на которые ссылаются поля. Данная форма коммуникации является предельно эффективной, но делает возможным возникновение ошибок двух разновидностей: вмешательство в поток (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-блок или метод, неявно получающий блокировку, или получением блокировки явным образом (таким как Volatile поляПрименительно к полям ключевое слово
Финальные поляПоле, которое объявлено final, называется финальным и не может быть изменено после инициализации. Финальные поля объекта инициализируются в его конструкторе. Если конструктор соответствует определённым простым правилам, то корректное значение финального поля будет видимо для остальных потоков без синхронизации. Простое правило: ссылка this не должна покинуть конструктор до его завершения. ИсторияНачиная с JDK 1.2, в Java включен стандартный набор классов-коллекций Java Collections Framework. Даг Ли, который также участвовал в реализации Java Collections Framework, разработал пакет concurrency, включающий в себя несколько примитивов синхронизации и большое количество классов, относящихся к коллекциям.[5] Работа над ним была продолжена как часть JSR 166[6] под председательством Дага Ли. Релиз JDK 5.0 включил много дополнений и пояснений к модели параллелизма в Java. Впервые API для работы с параллелизмом разарботанные JSR 166 были включены в JDK. JSR 133 предоставила поддержку для хорошо определённых атомарных операций в многопоточном/многопроцессорном окружении. И Java SE 6, и Java SE 7 привносят изменения и дополнения в JSR 166 API. См. также
Заметки
Ссылки
Ссылки на внешние ресурсы
|