Java-параллелизм
Эта статья включает список общих ссылок , но в ней отсутствуют достаточные соответствующие встроенные цитаты . ( Апрель 2010 г. ) |
Язык программирования Java и виртуальная машина Java (JVM) предназначены для поддержки параллельного программирования . Все выполнение происходит в контексте потоков . Доступ к объектам и ресурсам может осуществляться множеством отдельных потоков. Каждый поток имеет свой собственный путь выполнения, но потенциально может получить доступ к любому объекту в программе. Программист должен обеспечить правильную координацию (или « синхронизацию ») доступа к объектам для чтения и записи между потоками. [1] [2] Синхронизация потоков гарантирует, что объекты изменяются только одним потоком за раз, и предотвращает доступ потоков к частично обновленным объектам во время модификации другим потоком. [2] В языке Java есть встроенные конструкции для поддержки такой координации.
Процессы и потоки
[ редактировать ]Большинство реализаций виртуальной машины Java выполняются как один процесс . В языке программирования Java параллельное программирование в первую очередь связано с потоками (также называемыми облегченными процессами ). Несколько процессов можно реализовать только с помощью нескольких JVM.
Потоковые объекты
[ редактировать ]Потоки совместно используют ресурсы процесса, включая память и открытые файлы. Это обеспечивает эффективное, но потенциально проблематичное общение. [2] Каждое приложение имеет по крайней мере один поток, называемый основным потоком. Основной поток имеет возможность создавать дополнительные потоки как Runnable
или Callable
объекты. Callable
интерфейс похож на Runnable
в том, что оба предназначены для классов, экземпляры которых потенциально выполняются другим потоком. [3] А Runnable
, однако не возвращает результат и не может выдать проверенное исключение. [4]
Каждый поток можно запланировать [5] на другом ядре процессора [6] или используйте разделение времени на одном аппаратном процессоре или разделение времени на нескольких аппаратных процессорах. Не существует общего решения того, как потоки Java сопоставляются с собственными потоками ОС. Каждая реализация JVM может делать это по-своему.
Каждый поток связан с экземпляром класса Thread
. Потоками можно управлять либо напрямую с помощью Thread
объектов или косвенно, используя абстрактные механизмы, такие как Executor
или Task
с. [7]
Начало темы
[ редактировать ]Два способа запустить тему:
Предоставьте работоспособный объект
[ редактировать ] public class HelloRunnable implements Runnable { @Override public void run() { System.out.println("Hello from thread!"); } public static void main(String[] args) { (new Thread(new HelloRunnable())).start(); } }
Поток подкласса
[ редактировать ] public class HelloThread extends Thread { @Override public void run() { System.out.println("Hello from thread!"); } public static void main(String[] args) { (new HelloThread()).start(); } }
Прерывания
[ редактировать ]Прерывание сообщает потоку, что он должен остановить то, что он делает, и сделать что-то другое. Поток отправляет прерывание, вызывая interrupt()
на Thread
объект для прерывания потока. Механизм прерываний реализован с помощью внутреннего boolean
флаг, известный как «прерванный статус». [8] Вызов interrupt()
устанавливает этот флаг. [9] По соглашению, любой метод, который завершается путем выдачи InterruptedException
при этом очищает статус прерывания. Однако всегда возможно, что прерванное состояние будет немедленно установлено снова, вызывая другой поток. interrupt()
.
Присоединяется
[ редактировать ]java.lang.Thread#join()
метод позволяет один Thread
дождаться завершения другого.
Исключения
[ редактировать ]Неперехваченные исключения, вызванные кодом, завершают поток. Основной поток выводит исключения на консоль, но для созданных пользователем потоков требуется зарегистрированный обработчик. [10] [11]
Модель памяти
[ редактировать ]Модель памяти Java описывает, как потоки языка программирования Java взаимодействуют через память. На современных платформах код часто выполняется не в том порядке, в котором он был написан. Его порядок переупорядочивается компилятором , процессором и подсистемой памяти для достижения максимальной производительности. Язык программирования Java не гарантирует линеаризуемости или даже последовательной согласованности . [12] при чтении или записи полей общих объектов, и это делается для того, чтобы обеспечить оптимизацию компилятора (например, выделение регистров , устранение общего подвыражения и устранение избыточного чтения ), все из которых работают путем переупорядочения операций чтения и записи в памяти. [13]
Синхронизация
[ редактировать ]Потоки взаимодействуют главным образом путем совместного доступа к полям и объектам, на которые ссылаются поля. Эта форма связи чрезвычайно эффективна, но делает возможным два типа ошибок: помехи потоков и ошибки согласованности памяти. Инструментом, необходимым для предотвращения этих ошибок, является синхронизация.
Переупорядочение может вступить в игру в неправильно синхронизированных многопоточных программах, где один поток может наблюдать за эффектами других потоков и может обнаруживать, что доступ к переменным становится видимым для других потоков в другом порядке, чем выполняется или указан в программе.В большинстве случаев одному потоку все равно, что делает другой. Но когда это происходит, именно для этого и нужна синхронизация.
Для синхронизации потоков Java использует мониторы — высокоуровневый механизм, позволяющий одновременно только одному потоку выполнять область кода, защищенную монитором. Поведение мониторов объясняется с помощью блокировок ; с каждым объектом связана блокировка.
Синхронизация имеет несколько аспектов. Наиболее понятным является взаимное исключение : только один поток может одновременно удерживать монитор, поэтому синхронизация на мониторе означает, что как только один поток входит в синхронизированный блок, защищенный монитором, ни один другой поток не может войти в блок, защищенный этим монитором, до тех пор, пока первый поток выходит из синхронизированного блока. [2]
Но синхронизация – это нечто большее, чем просто взаимное исключение. Синхронизация гарантирует, что записи в память, выполняемые потоком до или во время синхронизированного блока, становятся видимыми предсказуемым образом для других потоков, которые синхронизируются на том же мониторе. После выхода из синхронизированного блока мы освобождаем монитор, в результате чего кэш сбрасывается в основную память, так что записи, сделанные этим потоком, могут быть видны другим потокам. Прежде чем мы сможем войти в синхронизированный блок, мы получаем монитор, что приводит к аннулированию кэша локального процессора, так что переменные будут перезагружены из основной памяти. После этого мы сможем увидеть все записи, сделанные видимыми в предыдущем выпуске.
Чтение — запись в поля линеаризуема , если либо поле является изменчивым , либо поле защищено уникальной блокировкой , которую получают все читатели и записывающие устройства.
Блокировки и синхронизированные блоки
[ редактировать ]Поток может добиться взаимного исключения либо путем входа в синхронизированный блок, либо в метод, который получает неявную блокировку. [14] [2] или путем получения явной блокировки (например, ReentrantLock
из java.util.concurrent.locks
упаковка [15] ). Оба подхода имеют одинаковые последствия для поведения памяти. Если все обращения к определенному полю защищены одной и той же блокировкой, то операции чтения и записи в это поле являются линеаризуемыми (атомарными).
Неустойчивые поля
[ редактировать ]При применении к полю Java volatile
Ключевое слово гарантирует, что:
- (Во всех версиях Java) Существует глобальный порядок операций чтения и записи в
volatile
переменная. Это означает, что каждый поток, обращающийся кvolatile
поле прочитает свое текущее значение перед продолжением вместо (потенциально) использования кэшированного значения. (Однако нет никаких гарантий относительно относительного порядка непостоянных операций чтения и записи с обычными операциями чтения и записи, а это означает, что в целом это бесполезная конструкция потоков.) - (В Java 5 или более поздних версиях) Волатильные операции чтения и записи устанавливают отношения «происходит до» , во многом похожие на получение и освобождение мьютекса. [16] Эта связь является просто гарантией того, что записи в память, выполненные одним конкретным оператором, видны другому конкретному оператору.
А volatile
поля линеаризуемы. Чтение volatile
поле похоже на получение блокировки: рабочая память становится недействительной и volatile
текущее значение поля пересчитывается из памяти. Написание volatile
поле похоже на снятие блокировки: volatile
поле немедленно записывается обратно в память.
Заключительные поля
[ редактировать ]Поле, объявленное как final
не может быть изменен после инициализации. [17] Объект final
поля инициализируются в его конструкторе. Пока this
ссылка не освобождается из конструктора до возвращения конструктора, тогда правильное значение любого final
поля будут видны другим потокам без синхронизации. [18]
История
[ редактировать ]Начиная с JDK 1.2 , Java включает стандартный набор классов коллекций, структуру коллекций Java.
Дуг Ли , который также участвовал в реализации платформы коллекций Java, разработал пакет параллелизма , включающий несколько примитивов параллелизма и большую батарею классов, связанных с коллекциями. [19] Эта работа была продолжена и обновлена в рамках JSR 166 под председательством Дуга Ли.
JDK 5.0 включает множество дополнений и уточнений в модель параллелизма Java. API-интерфейсы параллелизма, разработанные в JSR 166, также были впервые включены в состав JDK. JSR 133 обеспечивал поддержку четко определенных атомарных операций в многопоточной/многопроцессорной среде.
В выпусках Java SE 6 и Java SE 7 представлены обновленные версии API JSR 166, а также несколько новых дополнительных API.
См. также
[ редактировать ]- Параллелизм (информатика)
- Шаблон параллелизма
- Модель разветвления-объединения
- Барьер памяти
- Модели памяти
- Безопасность резьбы
- ThreadSafe
- Java ConcurrentMap
Примечания
[ редактировать ]- ^ Гетц и др. 2006 , стр. 15–17, §2 Потокобезопасность.
- ^ Гетц и др. 2006 , стр. 125–126, §6.3.2 Задачи, приносящие результат: вызываемые и будущие.
- ^ Гетц и др. 2006 , стр. 95–98, §5.5.2 FutureTask.
- ^ Bloch 2018 , стр. 336–337, глава §11, пункт 84. Не зависит от планировщика потоков.
- ^ Блох 2018 , с. 311, Глава §11 Параллелизм.
- ^ Bloch 2018 , стр. 323–324, Глава §5, пункт 80: Предпочитайте исполнителей, задачи и потоки потокам.
- ^ Гетц и др. 2006 , стр. 138–141, §7.1.1 Прерывание.
- ^ Гетц и др. 2006 , стр. 92–94, §5.4 Блокирующие и прерываемые методы.
- ^ Оракул. «Интерфейсный поток.UncaughtExceptionHandler» . Проверено 10 мая 2014 г.
- ^ «Тихая смерть потока из-за необработанных исключений» . literatejava.com . 10 мая 2014 года . Проверено 10 мая 2014 г.
- ^ Гетц и др. 2006 , стр. 338–339, §16.1.1 Модели памяти платформы.
- ^ Херлихи, Морис и Нир Шавит. «Искусство многопроцессорного программирования». ПОДК. Том. 6. 2006.
- ^ Гетц и др. 2006 , стр. 25–26, §2.3.1 Внутренние блокировки.
- ^ Гетц и др. 2006 , стр. 277–278, §13 Явные блокировки.
- ^ Раздел 17.4.4: Порядок синхронизации «Спецификация языка Java®, Java SE 7 Edition» . Корпорация Оракл . 2013 . Проверено 12 мая 2013 г.
- ^ Гетц и др. 2006 , с. 48, §3.4.1 Заключительные поля.
- ^ Гетц и др. 2006 , стр. 41–42, §3.2.1 Практика безопасного строительства.
- ^ Даг Ли . «Обзор пакета util.concurrent Release 1.3.4» . Проверено 1 января 2011 г.
Примечание. После выпуска J2SE 5.0 этот пакет переходит в режим обслуживания: будут выпущены только существенные исправления. Пакет J2SE5 java.util.concurrent включает улучшенные, более эффективные и стандартизированные версии основных компонентов этого пакета.
Ссылки
[ редактировать ]- Блох, Джошуа (2018). «Эффективная Java: Руководство по языку программирования» (третье изд.). Аддисон-Уэсли. ISBN 978-0134685991 .
- Гетц, Брайан; Пайерлс, Тим; Блох, Джошуа; Боубир, Джозеф; Холмс, Дэвид; Леа, Дуг (2006). Параллелизм Java на практике . Эддисон Уэсли. ISBN 0-321-34960-1 .
- Леа, Дуг (1999). Параллельное программирование на Java: принципы и шаблоны проектирования . Эддисон Уэсли. ISBN 0-201-31009-0 .