Глава 11
ПОТОКИ ВЫПОЛНЕНИЯ
Соседняя очередь всегда движется быстрее.
Наблюдение Этторе
Как только вы перейдете в другую очередь,
ваша начнет двигаться быстрее.
Наблюдение О’Брайена
Класс Thread и интерфейс Runnable
К большинству современных распределенных приложений (Rich Client)
и веб-приложений (Thin Client) выдвигаются требования одновременной под-
держки многих пользователей, каждому из которых выделяется отдельный по-
ток, а также разделения и параллельной обработки информационных ресурсов.
Потоки — средство, которое помогает организовать одновременное выполне-
ние нескольких задач, каждой в независимом потоке. Потоки представляют со-
бой экземпляры классов, каждый из которых запускается и функционирует
самостоятельно, автономно (или относительно автономно) от главного потока
выполнения программы. Существует два способа создания и запуска потока:
на основе расширения класса Thread или реализации интерфейса Runnable.
// # 1 # расширение класса Thread # TalkThread.java
package
by.bsu.threads;
public class
TalkThread extends Thread {
@Override
public
void run() {
for
(int i = 0; i < 10; i++) {
System.out.println("Talking");
try
{
Thread.sleep(7); // остановка на 7 миллисекунд
}
catch
(InterruptedException e) {
System.err.print(e);
}
}
}
}
При реализации интерфейса Runnable необходимо определить его единст-
венный абстрактный метод run(). Например:
ПОТОКИ ВЫПОЛНЕНИЯ
291
/* # 2 # реализация интерфейса Runnable # WalkRunnable.java # WalkTalk.java */
package
by.bsu.threads;
public class
WalkRunnable implements Runnable {
@Override
public
void run() {
for
( int i = 0; i < 10; i++) {
System. out.println("Walking");
try
{
Thread. sleep(7);
}
catch
(InterruptedException e) {
System. err.println(e);
}
}
}
}
package
by.bsu.threads;
public
class WalkTalk {
public
static void main(String[ ] args) {
//
новые объекты потоков
TalkThread talk = new TalkThread();
Thread walk = new Thread( new WalkRunnable());
//
запуск потоков
talk.start();
walk.start();
// WalkRunnable w = new WalkRunnable(); // просто объект, не поток
//
w.run(); или talk.run(); // выполнится метод, но поток не запустится!
}
}
Запуск двух потоков для объектов классов TalkThread непосредственно
и WalkRunnable через инициализацию экземпляра Thread приводит к выводу
строк: Talking Walking. Порядок вывода, как правило, различен при несколь-
ких запусках приложения.
Интерфейс Runnable не имеет метода start(), а только единственный метод
run(). Поэтому для запуска такого потока, как WalkRunnable следует создать
экземпляр класса Thread с передачей экземпляра WalkRunnable его конструк-
тору. Однако при прямом вызове метода run() поток не запустится, выполнится
только тело самого метода.
Жизненный цикл потока
При выполнении программы объект класса Thread может быть в одном
из четырех основных состояний: «новый», «работоспособный», «неработоспо-
собный» и «пассивный». При создании потока он получает состояние «новый»
( NEW) и не выполняется. Для перевода потока из состояния «новый» в состо-
яние «работоспособный» ( RUNNABLE) следует выполнить метод start(), ко-
торый вызывает метод run() — основной метод потока.
ИСПОЛЬЗОВАНИЕ КЛАССОВ И БИБЛИОТЕК
292
Поток может находиться в од-
ном из состояний, соответствую-
щих элементам статически вложен-
ного перечисления Thread.State:
NEW — поток создан, но еще
не запущен;
RUNNABLE — поток выпол-
няется;
BLOCKED — поток блокиро-
ван;
WAITING — поток ждет окон-
чания работы другого потока;
TIMED_WAITING — поток некоторое время ждет окончания другого потока;
TERMINATED — поток завершен.
Получить текущее значение состояния потока можно вызовом метода
getState().
Поток переходит в состояние «неработоспособный» в режиме ожидания
( WAITING) вызовом методов join(), wait(), suspend() (deprecated-метод) или
методов ввода/вывода, которые предполагают задержку. Для задержки потока
на некоторое время (в миллисекундах) можно перевести его в режим ожидания
по времени ( TIMED_WAITING) с помощью методов yield(), sleep(long millis),
join(long timeout) и wait(long timeout), при выполнении которых может гене-
рироваться прерывание InterruptedException. Вернуть потоку работоспособ-
ность после вызова метода suspend() можно методом resume() (deprecated-
метод), а после вызова метода wait() — методами notify() или notifyAll(). Поток
переходит в «пассивное» состояние ( TERMINATED), если вызваны методы
interrupt(), stop() (deprecated-метод) или метод run() завершил выполнение,
и запустить его повторно уже невозможно. После этого, чтобы запустить по-
ток, необходимо создать новый объект потока. Метод interrupt() успешно за-
вершает поток, если он находится в состоянии «работоспособный». Если же
поток неработоспособен, например, находится в состоянии TIMED_WAITING,
то метод инициирует исключение InterruptedException. Чтобы это не про-
исходило, следует предварительно вызвать метод isInterrupted(), который про-
верит возможность завершения работы потока. При разработке не следует ис-
пользовать методы принудительной остановки потока, так как возможны
проблемы с закрытием ресурсов и другими внешними объектами.
Методы suspend(), resume() и stop() являются deprecated-методами и запрещены
к использованию, так как они не являются в полной мере «потокобезопасными».
Рис. 11.1.
Состояния потока
ПОТОКИ ВЫПОЛНЕНИЯ
293
Управление приоритетами и группы потоков
Потоку можно назначить приоритет от 1 (константа MIN_PRIORITY)
до 10 ( MAX_PRIORITY) с помощью метода setPriority(int prior). Получить
значение приоритета потока можно с помощью метода getPriority().
// # 3 # демонстрация приоритетов # PriorityRunner.java # PriorThread.java
package
by.bsu.priority;
public
class PriorThread extends Thread {
public
PriorThread(String name) {
super
(name);
}
public
void run() {
for
( int i = 0; i < 71; i++) {
System. out.println(getName() + " " + i);
try
{
Thread. sleep(1); // попробовать sleep(0),sleep(10)
}
catch
(InterruptedException e) {
System. err.print(e);
}
}
}
}
package
by.bsu.priority;
public
class PriorityRunner {
public
static void main(String[ ] args) {
PriorThread min = new PriorThread("Min");
PriorThread max = new PriorThread("Max");
PriorThread norm = new PriorThread("Norm");
min.setPriority(Thread.MIN_PRIORITY); // 1
max.setPriority(Thread.MAX_PRIORITY); // 10
norm.setPriority(Thread.NORM_PRIORITY); // 5
min.start();
norm.start();
max.start();
}
}
Поток с более высоким приоритетом в данном случае, как правило, монопо-
лизирует вывод на консоль.
Потоки объединяются в группы потоков. После создания потока нельзя из-
менить его принадлежность к группе.
ThreadGroup tg = new ThreadGroup("Группа потоков 1");
Thread t0 = new Thread(tg, "поток 0");
Все потоки, объединенные в группу, имеют одинаковый приоритет. Чтобы опре-
делить, к какой группе относится поток, следует вызвать метод getThreadGroup().
ИСПОЛЬЗОВАНИЕ КЛАССОВ И БИБЛИОТЕК
294
Если поток до включения в группу имел приоритет выше приоритета группы
потоков, то после включения значение его приоритета станет равным приори-
тету группы. Поток же со значением приоритета, более низким, чем приоритет
группы после включения в оную, значения своего приоритета не изменит.
Управление потоками
Приостановить (задержать) выполнение потока можно с помощью метода
sleep(int millis) класса Thread. Менее надежный альтернативный способ состо-
ит в вызове метода yield(), который может сделать некоторую паузу и позволя-
ет другим потокам начать выполнение своей задачи. Метод join() блокирует
работу потока, в котором он вызван, до тех пор, пока не будет закончено выпол-
нение вызывающего метод потока или не истечет время ожидания при обраще-
нии к методу join(long timeout).
// # 4 # задержка потока # JoinRunner.java
package
by.bsu.management;
class
JoinThread extends Thread {
public
JoinThread (String name) {
super
(name);
}
public
void run() {
String nameT = getName();
long
timeout = 0;
System. out.println("Старт потока " + nameT);
try
{
switch
(nameT) {
case "First":
timeout = 5_000;
break;
case
"Second":
timeout = 1_000;
}
Thread. sleep(timeout);
System. out.println("завершение потока " + nameT);
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
}
public
class JoinRunner {
static
{
System. out.println("Старт потока main");
}
public
static void main(String[ ] args) {
JoinThread t1 = new JoinThread("First");
JoinThread t2 = new JoinThread("Second");
ПОТОКИ ВЫПОЛНЕНИЯ
295
t1.start();
t2.start();
try
{
t1.join()
; // поток main остановлен до окончания работы потока t1
}
catch
(InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread. сurrentThread().getName()); // имя текущего потока
}
}
Возможно, будет выведено:
Старт потока main
Старт потока First
Старт потока Second
завершение потока Second
завершение потока First
main
Несмотря на вызов метода join() для потока t1, поток t2 будет работать,
в отличие от потока main, который сможет продолжить свое выполнение толь-
ко по завершении потока t1.
Если вместо метода join() без параметров использовать версию join(long
timeout), то поток main будет остановлен только на указанный промежуток
времени. При вызове t1.join(500) вывод будет другим:
Старт потока First
Старт потока Second
main
завершение потока Second
завершение потока First
Статический метод
currentThread() возвращает ссылку на текущий поток,
т. е. на поток, в котором данный метод был вызван.
Вызов статического метода yield() для исполняемого потока должен приво-
дить к приостановке потока на некоторый квант времени, чтобы другие потоки
могли выполнять свои действия. Например, в случае потока с высоким приори-
тетом после обработки части пакета данных, когда следующая еще не готова,
стоит уступить часть времени другим потокам. Однако если требуется надеж-
ная остановка потока, то следует использовать его крайне осторожно или вооб-
ще применить другой способ.
// # 5 # задержка потока # YieldRunner.java
package
by.bsu.yield;
public
class YieldRunner {
public
static void main(String[ ] args) {
ИСПОЛЬЗОВАНИЕ КЛАССОВ И БИБЛИОТЕК
296
new
Thread() { // анонимный класс
public
void run() {
System. out.println("старт потока 1");
Thread.yield();
System. out.println("завершение 1");
}
}.start();
// запуск потока
new
Thread() {
public
void run() {
System. out.println("старт потока 2");
System. out.println("завершение 2");
}
}.start();
}
}
В результате может быть выведено:
старт потока 1
старт потока 2
завершение 2
завершение 1
Активизация метода yield() в коде метода run() первого объекта потока приве-
дет к тому, что, скорее всего, первый поток будет остановлен на некоторый квант
времени, что даст возможность другому потоку запуститься и выполнить свой код.
Потоки–демоны
Потоки-демоны используются для работы в фоновом режиме вместе с про-
граммой, но не являются неотъемлемой частью логики программы. Если ка-
кой-либо процесс может выполняться на фоне работы основных потоков вы-
полнения и его деятельность заключается в обслуживании основных потоков
приложения, то такой процесс может быть запущен как поток-демон. С помо-
щью метода setDaemon(boolean value), вызванного вновь созданным потоком
до его запуска, можно определить поток-демон. Метод boolean isDaemon() по-
зволяет определить, является ли указанный поток демоном или нет.
// # 6 # запуск и выполнение потока-демона # SimpleThread.java # DaemonRunner.java
package
by.bsu.daemons;
public class
SimpleThread extends Thread {
public
void run() {
try
{
if
(isDaemon()) {
System. out.println("старт потока-демона");
Thread. sleep(10_000); // заменить параметр на 1
ПОТОКИ ВЫПОЛНЕНИЯ
297
}
else
{
System. out.println("старт обычного потока");
}
}
catch
(InterruptedException e) {
System. err.print(e);
}
finally
{
if
(!isDaemon()) {
System. out.println("завершение обычного потока");
} else {
System. out.println("завершение потока-демона");
}
}
}
}
package
by.bsu.daemons;
public
class DaemonRunner {
public
static void main(String[ ] args) {
SimpleThread usual = new SimpleThread();
SimpleThread daemon = new SimpleThread();
daemon.setDaemon( true);
daemon.start();
usual.start();
System. out.println("последний оператор main");
}
}
В результате компиляции и запуска, возможно, будет выведено:
последний оператор main
старт потока-демона
старт обычного потока
завершение обычного потока
Поток-демон (из-за вызова метода sleep(10000)) не успел завершить выпол-
нение своего кода до завершения основного потока приложения, связанного
с методом main(). Базовое свойство потоков-демонов заключается в возможно-
сти основного потока приложения завершить выполнение потока-демона (в от-
личие от обычных потоков) с окончанием кода метода main(), не обращая вни-
мания на то, что поток-демон еще работает. Если уменьшать время задержки
потока-демона, то он может успеть завершить свое выполнение до окончания
работы основного потока.
Потоки и исключения
В процессе функционирования потоки являются в общем случае независи-
мыми друг от друга. Прямым следствием такой независимости будет коррект-
ное продолжение работы потока main после аварийной остановки запущенно-
го из него потока после генерации исключения.
ИСПОЛЬЗОВАНИЕ КЛАССОВ И БИБЛИОТЕК
298
/* # 7 # генерация исключения в созданном потоке # ExceptThread.java #
ExceptionThreadDemo.java */
package
by.bsu.thread;
public class
ExceptThread extends Thread {
public
void run() {
boolean
flag = true;
if
(flag) {
throw new RuntimeException();
}
System.out.println("end of ExceptThread");
}
}
package
by.bsu.thread;
public
class ExceptionThreadDemo {
public
static void main(String[ ] args) throws InterruptedException {
new ExceptThread().start();
Thread.sleep(1000);
System.out.println("end of main");
}
}
Основной поток избавлен от необходимости обрабатывать исключения
в порожденных потоках.
В данной ситуации верно и обратное: если основной поток прекратит свое
выполнение из-за необработанного исключения, то это не скажется на работо-
способности порожденного им потока.
/* # 8 # генерация исключения в потоке main # SimpleThread.java #
ExceptionMainDemo.java */
package
by.bsu.thread;
public class
SimpleThread extends Thread {
public
void run() {
try {
Thread.sleep(1000);
}
catch
(InterruptedException e) {
System.err.print(e);
}
System.out.println("end of SimpleThread");
}
}
package
by.bsu.thread;
public
class ExceptionMainDemo {
public
static void main(String[ ] args) {
new SimpleThread().start();
System.out.println("end of main with exception");
throw new RuntimeException();
}
}
ПОТОКИ ВЫПОЛНЕНИЯ
299
Атомарные типы и модификатор volatile
Все данные приложения находятся в основном хранилище данных. При за-
пуске нового потока создается копия хранилища и именно ею пользуется этот
поток. Изменения, произведенные в копии, могут не сразу находить отражение
в основном хранилище, и наоборот. Для получения актуального значения сле-
дует прибегнуть к синхронизации. Наиболее простым приемом будет объявле-
ние поля класса с модификатором volatile. Данный модификатор вынуждает
потоки производить действия по фиксации изменений достаточно быстро.
То есть другой заинтересованный поток, скорее всего, получит доступ к уже
измененному значению. Для базовых типов до 32 бит этого достаточно. При
использовании со ссылкой на объект — синхронизировано будет только значе-
ние самой ссылки, а не объект, на который она ссылается. Синхронизация
ссылки будет эффективной в случае, если она указывает на перечисление, так
как все элементы перечисления существуют в единственном экземпляре.
Решением проблемы с доступом к одному экземпляру из разных потоков явля-
ется блокирующая синхронизация. Модификатор volatile обеспечивает небло-
кирующую синхронизацию.
Существует целая группа классов пакета java.util.concurrent.atomic, обеспе-
чивающая неблокирующую синхронизацию. Атомарные классы созданы для
организации неблокирующих структур данных. Классы атомарных перемен-
ных AtomicInteger, AtomicLong, AtomicReference и др. расширяют нотацию
volatile значений, полей и элементов массивов. Все атомарные классы являют-
ся изменяемыми в отличие от соответствующих им классов-оболочек. При ре-
ализации классов пакета использовались эффективные атомарные инструкции
машинного уровня, которые доступны на современных процессорах. В некото-
рых ситуациях могут применяться варианты внутреннего блокирования.
Экземпляры классов, например, AtomicInteger и AtomicReference, предо-
ставляют доступ и разного рода обновления к одной-единственной перемен-
ной соответствующего типа. Каждый класс также обеспечивает набор методов
для этого типа. В частности, класс AtomicInteger — атомарные методы инкре-
мента и декремента. Инструкции при доступе и обновлении атомарных пере-
менных, в общем, следуют правилам для volatile.
Не следует классы атомарных переменных использовать как замену соот-
ветствующих классов-оболочек.
Пусть имеется некоторая торговая площадка, представленная классом
Market, работающая в непрерывном режиме и информирующая о разнона-
правленных изменениях биржевого индекса (поле index типа AtomicLong)
дважды за один цикл с интервалом до 500 миллисекунд. Изменения поля index
фиксируются методом addAndGet(long delta) атомарного добавления пере-
данного значения к текущему.
ИСПОЛЬЗОВАНИЕ КЛАССОВ И БИБЛИОТЕК
300
/* # 9 # класс с атомарным полем # Market.java */
package
by.bsu.market;
import
java.util.Random;
import
java.util.concurrent.atomic.AtomicLong;
public
class Market extends Thread {
private
AtomicLong index;
public
Market(AtomicLong index) {
this.index = index;
}
Достарыңызбен бөлісу: |