Язык программирования Java и виртуальная машина Java (JVM) были разработаны для поддержки параллельного программирования, и все выполнение происходит в контексте потоков. Доступ к объектам и ресурсам может осуществляться многими отдельными потоками; каждый поток имеет свой собственный путь выполнения, но потенциально может получить доступ к любому объекту в программе. Программист должен гарантировать, что доступ для чтения и записи к объектам должным образом скоординирован (или "синхронизирован ") между потоками. Синхронизация потоков гарантирует, что объекты изменяются только одним потоком за раз и что потоки не могут получить доступ к частично обновленным объектам во время модификации другим потоком. В языке Java есть встроенные конструкции для поддержки такой координации.
Большинство реализаций виртуальной Java машина выполняется как единый процесс, а в языке программирования Java параллельное программирование в основном связано с потоками (также называемыми легковесными процессами ). Несколько процессов могут быть реализованы только с несколькими JVM.
Потоки совместно используют ресурсы процесса, включая память и открытые файлы. Это делает общение эффективным, но потенциально проблематичным. В каждом приложении есть хотя бы один поток, называемый основным потоком. Основной поток имеет возможность создавать дополнительные потоки как объекты Runnable
или Callable
. (Интерфейс Callable
аналогичен интерфейсу Runnable
в том, что оба они предназначены для классов, экземпляры которых потенциально выполняются другим потоком. A Runnable
, однако, не возвращает результат и не может генерировать проверенное исключение.)
Каждый поток может быть запланирован на другом ядре ЦП или использовать квантование времени на одном аппаратном процессоре или квантование времени на многих аппаратных процессорах. Не существует универсального решения того, как потоки Java отображаются в собственные потоки ОС. Каждая реализация JVM может делать это по-своему.
Каждый поток связан с экземпляром класса Thread. Потоками можно управлять либо напрямую с помощью объектов Thread, либо с помощью абстрактных механизмов, таких как коллекции Executor
и java.util.concurrent
.
Два способа запустить поток:
открытый класс HelloRunnable реализует Runnable {@Override public void run () {System.out.println ("Привет из потока!"); } public static void main (String args) {(новый поток (новый HelloRunnable ())). start (); }}
открытый класс HelloThread расширяет поток {@Override public void run () {System.out.println ("Hello from thread!"); } public static void main (String args) {(new HelloThread ()). start (); }}
Прерывание - это указание потоку, что он должен остановить то, что он делает, и сделать что-то еще. Поток отправляет прерывание, вызывая прерывание для объекта Thread, чтобы поток был прерван. Механизм прерывания реализован с использованием внутреннего флага, известного как состояние прерывания. Вызов Thread.interrupt
устанавливает этот флаг. По соглашению, любой метод, который завершается путем выдачи исключения InterruptedException
, при этом очищает состояние прерывания. Однако всегда возможно, что статус прерывания будет немедленно снова установлен другим потоком, вызывающим прерывание.
Методы Thread.join
позволяют одному потоку ждать завершения другого.
Неперехваченные исключения, вызванные кодом, завершат поток. Основной поток выводит исключения на консоль, но для созданных пользователем потоков требуется зарегистрированный обработчик.
Модель памяти Java описывает, как потоки в языке программирования Java взаимодействуют через память. На современных платформах код часто выполняется не в том порядке, в котором он был написан. Он переупорядочивается компилятором, процессором и подсистемой памяти для достижения максимальной производительности. Язык программирования Java не гарантирует линеаризуемость или даже последовательную согласованность при чтении или записи полей общих объектов, и это сделано для оптимизации компилятора ( такие как выделение регистров, исключение общего подвыражения и исключение избыточного чтения ), все из которых работают путем переупорядочения операций чтения-записи в памяти.
Потоки общаются в основном путем совместного использования доступа к полям и объектам, на которые ссылаются поля. Эта форма связи чрезвычайно эффективна, но делает возможными два типа ошибок: интерференцию потоков и ошибки согласованности памяти. Инструмент, необходимый для предотвращения этих ошибок, - это синхронизация.
Переупорядочение может применяться в неправильно синхронизированных многопоточных программах, где один поток может наблюдать эффекты других потоков и может обнаруживать эту переменную доступы становятся видимыми для других потоков в порядке, отличном от порядка выполнения или указанного в программе. В большинстве случаев один поток не заботится о том, что делает другой. Но когда это происходит, для этого и нужна синхронизация.
Для синхронизации потоков Java использует мониторы, которые представляют собой высокоуровневый механизм, позволяющий только одному потоку одновременно выполнять область кода, защищенную монитором. Поведение мониторов объясняется с помощью блокировок ; с каждым объектом связана блокировка.
Синхронизация имеет несколько аспектов. Наиболее понятным является взаимное исключение - только один поток может одновременно удерживать монитор, поэтому синхронизация на мониторе означает, что как только один поток входит в синхронизированный блок, защищенный монитором, никакой другой поток не может войти в блок, защищенный этим монитором, пока первый поток не выйдет из синхронизированного блока.
Но синхронизация - это не только взаимное исключение. Синхронизация гарантирует, что записи в память, выполняемые потоком до или во время синхронизированного блока, становятся видимыми предсказуемым образом для других потоков, которые синхронизируются на том же мониторе. После выхода из синхронизированного блока мы освобождаем монитор, что приводит к сбросу кеша в основную память, так что записи, сделанные этим потоком, могут быть видны другим потокам. Прежде чем мы сможем ввести синхронизированный блок, мы приобретаем монитор, который приводит к аннулированию кеша локального процессора, так что переменные будут перезагружены из основной памяти. После этого мы сможем увидеть все записи, сделанные в предыдущем выпуске.
Чтение - запись в поля линеаризуема, если либо поле имеет значение volatile, либо поле защищено уникальной блокировкой, которая получена всеми читателями и писателями.
Поток может добиться взаимного исключения либо путем ввода синхронизированного блока или метода, который получает неявную блокировку, либо путем получения явной блокировки (например, ReentrantLock из пакет java.util.concurrent.locks). Оба подхода имеют одинаковые последствия для поведения памяти. Если все обращения к определенному полю защищены одной и той же блокировкой, то операции чтения-записи в это поле будут линеаризуемыми (атомарными).
При применении к полю Java volatile
гарантирует, что:
Изменяемые поля являются линеаризуемыми. Чтение изменчивого поля похоже на получение блокировки: рабочая память становится недействительной, и текущее значение изменчивого поля повторно считывается из памяти. Запись изменчивого поля похожа на снятие блокировки: изменчивое поле немедленно записывается обратно в память.
Поле, объявленное как окончательное, не может быть изменено после его инициализации. Конечные поля объекта инициализируются в его конструкторе. Если конструктор следует некоторым простым правилам, то правильное значение любых полей final будет видно другим потокам без синхронизации. Правило простое: ссылка this
не должна выходить из конструктора до того, как конструктор вернется.
Начиная с JDK 1.2, Java включает стандартный набор классов коллекций, структуру коллекций Java
Дуг Ли, который также участвовал в реализации структуры коллекций Java, разработал пакет параллелизма , включающий несколько примитивов параллелизма и большую батарею классов, связанных с коллекциями. Эта работа была продолжена и обновлена как часть JSR 166, который возглавлял Дуг Ли.
JDK 5.0 включает в себя множество дополнений и уточнений к модели параллелизма Java. API-интерфейсы параллелизма, разработанные JSR 166, также впервые были включены как часть JDK. JSR 133 обеспечивает поддержку четко определенных атомарных операций в многопоточной / многопроцессорной среде.
В выпусках Java SE 6 и Java SE 7 представлены обновленные версии API-интерфейсов JSR 166, а также несколько новых дополнительных API-интерфейсов.