- Многопоточность Thread, Runnable
- Преимущества потоков перед процессами
- Главный поток
- Класс Thread
- Конструкторы класса Thread
- Методы класса Thread
- Жизненный цикл потока
- Пример использования Thread
- Интерфейс Runnable
- Пример класса RunnableExample, реализующего интерфейс Runnable
- Синхронизация потоков, synchronized
- Блокировка на уровне объекта
- Блокировка на уровне метода и класса
- Некоторые важные замечания использования synchronized
- Взаимная блокировка
- Взаимодействие между потоками в Java, wait и notify
- Листинг класса Store
- BestProg
- Средства языка Java для работы с потоками выполнения. Класс Thread . Интерфейс Runnable . Главный поток выполнения. Создание дочернего потока
- Содержание
Многопоточность Thread, Runnable
Многопоточное программирование позволяет разделить представление и обработку информации на несколько «легковесных» процессов (light-weight processes), имеющих общий доступ как к методам различных объектов приложения, так и к их полям. Многопоточность незаменима в тех случаях, когда графический интерфейс должен реагировать на действия пользователя при выполнении определенной обработки информации. Потоки могут взаимодействовать друг с другом через основной «родительский» поток, из которого они стартованы.
В качестве примера можно привести некоторый поток, отвечающий за представление информации в интерфейсе, который ожидает завершения работы другого потока, загружающего файл, и одновременно отображает некоторую анимацию или обновляет прогресс-бар. Кроме того этот поток может остановить загружающий файл поток при нажатии кнопки «Отмена».
Создатели Java предоставили две возможности создания потоков: реализация (implementing) интерфейса Runnable и расширение(extending) класса Thread. Расширение класса — это путь наследования методов и переменных класса родителя. В этом случае можно наследоваться только от одного родительского класса Thread. Данное ограничение внутри Java можно преодолеть реализацией интерфейса Runnable, который является наиболее распространённым способом создания потоков.
Преимущества потоков перед процессами
- потоки намного легче процессов поскольку требуют меньше времени и ресурсов;
- переключение контекста между потоками намного быстрее, чем между процессами;
- намного проще добиться взаимодействия между потоками, чем между процессами.
Главный поток
Каждое java приложение имеет хотя бы один выполняющийся поток. Поток, с которого начинается выполнение программы, называется главным. После создания процесса, как правило, JVM начинает выполнение главного потока с метода main(). Затем, по мере необходимости, могут быть запущены дополнительные потоки. Многопоточность — это два и более потоков, выполняющихся одновременно в одной программе. Компьютер с одноядерным процессором может выполнять только один поток, разделяя процессорное время между различными процессами и потоками.
Класс Thread
В классе Thread определены семь перегруженных конструкторов, большое количество методов, предназначенных для работы с потоками, и три константы (приоритеты выполнения потока).
Конструкторы класса Thread
- target – экземпляр класса реализующего интерфейс Runnable;
- name – имя создаваемого потока;
- group – группа к которой относится поток.
Пример создания потока, который входит в группу, реализует интерфейс Runnable и имеет свое уникальное название :
Группы потоков удобно использовать, когда необходимо одинаково управлять несколькими потоками. Например, несколько потоков выводят данные на печать и необходимо прервать печать всех документов поставленных в очередь. В этом случае удобно применить команду ко всем потокам одновременно, а не к каждому потоку отдельно. Но это можно сделать, если потоки отнесены к одной группе.
Несмотря на то, что главный поток создаётся автоматически, им можно управлять. Для этого необходимо создать объект класса Thread вызовом метода currentThread().
Методы класса Thread
Наиболее часто используемые методы класса Thread для управления потоками :
- long getId() — получение идентификатора потока;
- String getName() — получение имени потока;
- int getPriority() — получение приоритета потока;
- State getState() — определение состояния потока;
- void interrupt() — прерывание выполнения потока;
- boolean isAlive() — проверка, выполняется ли поток;
- boolean isDaemon() — проверка, является ли поток «daemon»;
- void join() — ожидание завершения потока;
- void join(millis) — ожидание millis милисекунд завершения потока;
- void notify() — «пробуждение» отдельного потока, ожидающего «сигнала»;
- void notifyAll() — «пробуждение» всех потоков, ожидающих «сигнала»;
- void run() — запуск потока, если поток был создан с использованием интерфейса Runnable;
- void setDaemon(bool) — определение «daemon» потока;
- void setPriority(int) — определение приоритета потока;
- void sleep(int) — приостановка потока на заданное время;
- void start() — запуск потока.
- void wait() — приостановка потока, пока другой поток не вызовет метод notify();
- void wait(millis) — приостановка потока на millis милисекунд или пока другой поток не вызовет метод notify();
Жизненный цикл потока
При выполнении программы объект Thread может находиться в одном из четырех основных состояний: «новый», «работоспособный», «неработоспособный» и «пассивный». При создании потока он получает состояние «новый» (NEW) и не выполняется. Для перевода потока из состояния «новый» в «работоспособный» (RUNNABLE) следует выполнить метод start(), вызывающий метод run().
Поток может находиться в одном из состояний, соответствующих элементам статически вложенного перечисления Thread.State :
NEW — поток создан, но еще не запущен;
RUNNABLE — поток выполняется;
BLOCKED — поток блокирован;
WAITING — поток ждет окончания работы другого потока;
TIMED_WAITING — поток некоторое время ждет окончания другого потока;
TERMINATED — поток завершен.
Пример использования Thread
В примере ChickenEgg рассматривается параллельная работа двух потоков (главный поток и поток Egg), в которых идет спор, «что было раньше, яйцо или курица?». Каждый поток высказывает свое мнение после небольшой задержки, формируемой методом ChickenEgg.getTimeSleep(). Побеждает тот поток, который последним говорит свое слово.
При выполнении программы в консоль было выведено следующее сообщение.
Невозможно точно предсказать, какой поток закончит высказываться последним. При следующем запуске «победитель» может измениться. Это происходит вследствии так называемого «асинхронного выполнения кода». Асинхронность обеспечивает независимость выполнения потоков. Или, другими словами, параллельные потоки независимы друг от друга, за исключением случаев, когда бизнес-логика зависимости выполнения потоков определяется предусмотренными для этого средств языка.
Интерфейс Runnable
Интерфейс Runnable содержит только один метод run() :
Метод run() выполняется при запуске потока. После определения объекта Runnable он передается в один из конструкторов класса Thread.
Пример класса RunnableExample, реализующего интерфейс Runnable
При выполнении программы в консоль было выведено следующее сообщение.
Синхронизация потоков, synchronized
В процессе функционирования потоки часто используют общие ресурсы приложения, определенные вне потока. Если несколько потоков начнут одновременно вносить изменения в общий ресурс, то результаты выполнения программы могут быть непредсказуемыми. Рассмотрим следующий пример :
В примере определен общий ресурс в виде класса CommonObject, в котором имеется целочисленное поле counter. Данный ресурс используется внутренним классом, создающим поток CounterThread для увеличения в цикле значения counter на единицу. При старте потока полю counter присваивается значение 1. После завершения работы потока значение res.counter должно быть равно 4.
Две строчки кода класса CounterThread закомментированы. О них речь пойдет ниже.
В главном классе программы SynchronizedThread.main запускается пять потоков. То есть, каждый поток должен в цикле увеличить значение res.counter с единицы до четырех; и так пять раз. Но результат работы программы, отображаемый в консоли, будет иным :
То есть, с общим ресурсов res.counter работают все потоки одновременно, поочередно изменяя значение.
Чтобы избежать подобной ситуации, потоки необходимо синхронизировать. Одним из способов синхронизации потоков связан с использованием ключевого слова synchronized. Оператор synchronized позволяет определить блок кода или метод, который должен быть доступен только одному потоку. Можно использовать synchronized в своих классах определяя синхронизированные методы или блоки. Но нельзя использовать synchronized в переменных или атрибутах в определении класса.
Блокировка на уровне объекта
Блокировать общий ресурс можно на уровне объекта, но нельзя использовать для этих целей примитивные типы. В примере следует удалить строчные комментарии в классе CounterThread, после чего общий ресурс будет блокироваться как только его захватит один из потоков; остальные потоки будут ждать в очереди освобождения ресурса. Результат работы программы при синхронизации доступа к общему ресурсу резко изменится :
Следующий код демонстрирует порядок использования оператора synchronized для блокирования доступа к объекту.
Блокировка на уровне метода и класса
Блокировать доступ к ресурсам можно на уровне метода и класса. Следующий код показывает, что если во время выполнения программы имеется несколько экземпляров класса DemoClass, то только один поток может выполнить метод demoMethod(), для других потоков доступ к методу будет заблокирован. Это необходимо когда требуется сделать определенные ресурсы потокобезопасными.
Каждый объект в Java имеет ассоциированный с ним монитор, который представляет своего рода инструмент для управления доступа к объекту. Когда выполнение кода доходит до оператора synchronized, монитор объекта блокируется, предоставляя монопольный доступ к блоку кода только одному потоку, который произвел блокировку. После окончания работы блока кода, монитор объекта освобождается и он становится доступным для других потоков.
Некоторые важные замечания использования synchronized
- Синхронизация в Java гарантирует, что два потока не могут выполнить синхронизированный метод одновременно.
- Оператор synchronized можно использовать только с методами и блоками кода, которые могут быть как статическими, так и не статическими.
- Если один из потоков начинает выполнять синхронизированный метод или блок, то этот метод/блок блокируются. Когда поток выходит из синхронизированного метода или блока JVM снимает блокировку. Блокировка снимается, даже если поток покидает синхронизированный метод после завершения из-за каких-либо ошибок или исключений.
- Синхронизация в Java вызывает исключение NullPointerException, если объект, используемый в синхронизированном блоке, не определен, т.е. равен null.
- Синхронизированные методы в Java вносят дополнительные затраты на производительность приложения. Поэтому следует использовать синхронизацию, когда она абсолютно необходима.
- В соответствии со спецификацией языка нельзя использовать synchronized в конструкторе, т.к. приведет к ошибке компиляции.
Примечание : для синхронизации потоков можно использовать объекты синхронизации Synchroniser’s пакета java.util.concurrent.
Взаимная блокировка
С использованием блокировок необходимо быть очень внимательным, чтобы не создать «взаимоблокировку», которая хорошо известна разработчикам. Этот термин означает, что один из потоков ждет от другого освобождения заблокированного им ресурса, в то время как сам также заблокировал один из ресурсов, доступа к которому ждёт второй поток. В данном процессе могут участвовать два и более потоков.
Основные условия возникновения взаимоблокировок в многопотоковом приложении :
- наличие ресурсов, которые должны быть доступны только одному потоку в произвольный момент времени;
- при захвате ресурса поток пытается захватить еще один уникальный ресурс;
- отсутствует механизм освобождения ресурса при продолжительном его удержании;
- во время исполнения несколько потоков могут захватить разные уникальные ресурсы и ждать друг от друга их освобождения.
Взаимодействие между потоками в Java, wait и notify
При взаимодействии потоков часто возникает необходимость приостановки одних потоков и их последующего извещения о завершении определенных действий в других потоков. Так например, действия первого потока зависят от результата действий второго потока, и надо каким-то образом известить первый поток, что второй поток произвел/завершил определенную работу. Для подобных ситуаций используются методы :
- wait() — освобождает монитор и переводит вызывающий поток в состояние ожидания до тех пор, пока другой поток не вызовет метод notify();
- notify() — продолжает работу потока, у которого ранее был вызван метод wait();
- notifyAll() — возобновляет работу всех потоков, у которых ранее был вызван метод wait().
Все эти методы вызываются только из синхронизированного контекста (синхронизированного блока или метода).
Рассмотрим пример «Производитель-Склад-Потребитель» (Producer-Store-Consumer). Пока производитель не поставит на склад продукт, потребитель не может его забрать. Допустим производитель должен поставить 5 единиц определенного товара. Соответственно потребитель должен весь товар получить. Но, при этом, одновременно на складе может находиться не более 3 единиц товара. При реализации данного примера используем методы wait() и notify().
Листинг класса Store
Класс Store содержит два синхронизированных метода для получения товара get() и для добавления товара put(). При получении товара выполняется проверка счетчика counter. Если на складе товара нет, то есть counter
Источник
BestProg
Средства языка Java для работы с потоками выполнения. Класс Thread . Интерфейс Runnable . Главный поток выполнения. Создание дочернего потока
Содержание
Поиск на других ресурсах:
1. Способы создания потоков выполнения
В языке Java поток выполнения можно создавать одним из двух способов:
- путем расширения класса Thread . Этот класс является основным классом, на котором построена многопоточная система Java. Класс Thread определяет один поток выполнения. Если в программе нужно создать три потока выполнения, то, соответственно, создается три экземпляра класса Thread;
- за счет реализации интерфейса Runnable . Интерфейс Runnable дополняет возможности класса Thread .
2. Класc Thread . Конструкторы. Обзор методов
Класс Thread является основным классом при работе с потоками выполнения. Создание любого потока начинается с создания экземпляра класса Thread . При этом можно использовать следующие конструкторы класса
- threadObject – объект некоторого класса, который нужно выполнить в потоке. Этот объект должен реализовывать интерфейс Runnable (смотрите ниже) или расширять класс Thread ;
- threadName – имя, которое устанавливается для созданного потока исполнения. Это имя может быть прочитано методом getName() .
В общем случае, код создания потока выполнения с именем «My thread» для объекта класса MyThreadClass выглядит следующим образом:
Класс Thread содержит ряд методов для работы с потоками выполнения. Ниже приведено описание этих методов в классе:
- getName() — получить имя потока выполнения;
- getPriority() — получить приоритет потока выполнения;
- isAlive() — определить, выполняется ли поток;
- join() — предназначен для ожидания завершения потока выполнения;
- run() — задает код, который должен выполняться в потоке выполнения. Это есть точка входа в поток;
- sleep() — приостанавливает выполнение вызывающего потока выполнения на указанное время;
- start() — запускает поток выполнения с помощью вызова метода run() .
Примеры использования вышеприведенных методов можно изучить здесь и здесь .
3. Интерфейс Runnable . Особенности использования
Интерфейс Runnable используется для создания потока выполнения. Поток выполнения можно создать из объекта класса, реализующего интерфейс Runnable .
Интерфейс объявляет один метод run() , который имеет следующую общую форму:
Методом run() определяется точка входа в поток. Как только завершится выполнение метода run() , завершится и выполнения потока. В методе run() можно выполнять любые операции, присущие обычным методам (объявлять переменные, использовать классы, вызывать другие методы и т.д.).
В общем случае, создание потока выполнения для класса MyThreadClass выглядит следующим образом:
4. Главный поток выполнения. Особенности. Доступ к главному потоку выполнения
После запуска на выполнение программы, начинает выполняться один поток — главный поток программы. Для главного потока можно выделить следующие характерные особенности:
- главный поток начинает выполняться первым;
- из главного потока можно породить (создать) все дочерние потоки;
- желательно, чтобы главный поток завершался последним и выполнял некоторые завершающие действия при закрытии программы.
Чтобы получить доступ к главному потоку управления, нужно создать экземпляр класса Thread с помощью вызова статического метода currentThread() . После этого, с помощью экземпляра, можно использовать дополнительные функции управления главным потоком (приостановить главный поток, получить информацию о главном потоке и т.д.).
5. Пример, демонстрирующий доступ к главному потоку выполнения
В примере продемонстрирован доступ к главному потоку в программе и его использование.
Результат выполнения программы
6. Пример, демонстрирующий создание дочернего потока путем реализации интерфейса Runnable
Одним из способов создания дочернего потока является реализация интерфейса Runnable . Если класс использует интерфейс Runnable , то в этом классе нужно реализовывать метод run() .
Результат выполнения программы
7. Пример, демонстрирующий создание дочернего потока путем расширения класса Thread
Другой способ создания потока — наследование (расширение) класса Thread . При этом способе доступны некоторые методы суперкласса Thread . Более подробно об использовании методов класса Thread описывается здесь.
Источник