Подробности, касающиеся wait и notify
Оглавление
Создание потоков. 2
Синхронизация. 3
Операторы synchronized. 4
Методы wait и notify. 6
Подробности, касающиеся wait и notify. 7
Планирование потоков. 8
Приостановка потоков. 11
Прерывание потока. 11
Завершение работы потока. 12
Использование Runnable. 14
Ключевое слово volatile. 15
Варианты заданий: 16
Создание потоков
Чтобы породить новый поток выполнения, для начала следует создать объект Thread:
Thread worker = new Thread();
После того как объект-поток будет создан, вы можете задать его конфигурацию и запустить. В понятие конфигурации потока входит указание исходного приоритета, имени и так далее. Когда поток готов к работе, следует вызвать его метод start. Метод start порождает новый выполняемый поток на основе данных объекта класса Thread, после чего завершается. Метод start вызывает метод run нового потока, что приводит к активизации последнего.
Выход из метода run означает прекращение работы потока. Поток можно завершить и явно, посредством вызова stop; его выполнение может быть приостановлено методом suspend; существуют много других средств для работы с потоками, которые мы вскоре рассмотрим.
Стандартная реализация Thread.run не делает ничего. Вы должны либо расширить класс Thread, чтобы включить в него новый метод run, либо создать объект Runnable и передать его конструктору потока.
Приведенная ниже простая программа задействует два потока, которые выводят слова «ping» и «PONG » с различной частотой:
class PingPong extends Thread {
String word; // выводимое слово
int delay; // длительность паузы
PingPong(String whatToSay, int delayTime) {
word = whatToSay;
delay = delayTime;
}
public void run() {
try {
for (;;) {
System.out.print(word + " ");
sleep(delay); // подождать следующего вывода
}
} catch (InterruptedException e) {
return;
}
}
public static void main(String[] args) {
new PingPong("ping", 33).start(); // 1/30 секунды
new PingPong("PONG", 100).start(); // 1/10 секунды
}
}
Мы определили тип потока с именем PingPong. Его метод run работает в бесконечном цикле, выводя содержимое поля word и делая паузу на delay микросекунд. Метод PingPong.run не может возбуждать исключений, поскольку этого не делает переопределяемый им метод Thread.run. Соответственно, мы должны перехватить исключение InterruptedException, которое может возбуждаться методом sleep.
После этого можно непосредственно создать выполняющиеся потоки — именно это и делает метод PingPong. Он конструирует два объекта PingPong, каждый из которых обладает своим выводимым словом и интервалом задержки, после чего вызывает методы start обоих объектов-потоков. С этого момента и начинается работа потоков. Примерный результат работы может выглядеть следующим образом:
ping PONG ping ping PONG ping ping ping PONG ping
ping PONG ping ping ping PONG ping ping PONG ping
ping ping PONG ping ping ping PONG ping ping PONG
ping ping ping PONG ping ping ping PONG ping ping
PONG ping ping ping PONG ping ping ping PONG ping
ping ping PONG ping ping PONG ping ping ping PONG ...
Поток может обладать именем, которое передается в виде параметра типа String либо конструктору, либо методу setName. Вы получите текущее имя потока, если вызовете метод getName. Имена потоков предусмотрены исключительно для удобства программиста — в системе runtime в Java они не используются.
Вызов статического метода Thread.currentThread позволяет получить объект Thread, который соответствует работающему в настоящий момент потоку.
Синхронизация
Чтобы класс мог использоваться в многопоточной среде, необходимо объявить соответствующие методы с атрибутом synchronized. Если некоторый поток вызывает метод synchronized, то происходит блокировка объекта. Вызов метода synchronized того же объекта другим потоком будет приостановлен до снятия блокировки.
Синхронизация приводит к тому, что выполнение двух потоков становится взаимно исключающим по времени. Проблема вложенных вызовов решается очевидным образом: если синхронизированный метод вызывается для объекта, который ранее был заблокирован тем же самым потоком, то метод выполняется, однако блокировка не снимается вплоть до выхода из самого внешнего синхронизированного метода.
Синхронизация решает проблему, возникающую в нашем примере: если действия выполняются в синхронизированном методе, то при попытке обращения к объекту со стороны второго потока в тот момент, когда с объектом работает первый поток, доступ будет отложен до снятия блокировки.
Приведем пример того, как мог бы выглядеть класс Account, спроектированный для работы в многопоточной среде:
class Account {
private double balance;
public Account(double initialDeposit) {
balance = initialDeposit;
}
public synchronized double getBalance() {
return balance;
}
public synchronized void deposit(double amount) {
balance += amount;
}
}
А теперь мы объясним, что же означает понятие «соответствующие » применительно к синхронизированным методам.
Конструктор не обязан быть synchronized, поскольку он выполняется только при создании объекта, а это может происходить только в одном потоке для каждого вновь создаваемого объекта. Поле balance защищено от любых несинхронных изменений за счет использования методов доступа, объявленных synchronized. В этом заключается еще одна причина, по которой вместо объявления полей public или protected следует применять методы для работы с ними: так вы сможете контролировать синхронизацию доступа к ним.
Если поле может измениться, оно никогда не должно считываться в тот момент, когда другой поток производит запись. Доступ к полям должен быть синхронизирован. Если бы один поток считывал значение поля, пока другой поток его устанавливает, то в результате могло бы получиться частично искаженное значение. Объявление synchronized гарантирует, что два (или более) потока не будут вмешиваться в работу друг друга. Тем не менее, на порядок выполнения операций не дается никаких гарантий; если сначала произойдет чтение, то оно закончится до того, как начнется запись, и наоборот. Если же вы хотите, чтобы все происходило в строго определенном порядке, работа потоков должна координироваться способом, зависящим от конкретного приложения.
Методы класса также могут синхронизироваться с использованием блокировки на уровне класса. Два потока не могут одновременно выполнять синхронизированные статические методы одного класса. Блокировка статического метода на уровне класса не отражается на объектах последнего — вы можете вызвать синхронизированный метод для объекта, пока другой поток заблокировал весь класс в синхронизированном статическом методе. В последнем случае блокируются только синхронизированные статические методы.
Если синхронизированный метод переопределяется в расширенном классе, то новый метод не обязан быть синхронизированным. Метод суперкласса при этом остается синхронизированным, так что несинхронность метода в расширенном классе не отменяет его синхронизированного поведения в суперклассе. Если в несинхронизированном методе используется конструкция super.method() для обращения к методу суперкласса, то объект блокируется на время вызова до выхода из метода суперкласса.
Операторы synchronized
Оператор synchronized позволяет выполнить синхронизированный фрагмент программы, который осуществляет блокировку объекта, не требуя от программиста вызова синхронизированного метода для данного объекта. Оператор synchronized состоит из двух частей: указания блокируемого объекта и оператора, выполняемого после получения блокировки. Общая форма оператора synchronized выглядит следующим образом:
synchronized (выражение)
оператор
Взятое в скобки выражение должно указывать на блокируемый объект — обычно оно является ссылкой на объект. После блокировки выполняется оператор — так, словно для данного объекта выполняется синхронизированный метод. Чаще всего при блокировке объекта необходимо выполнить сразу несколько операторов, так что оператор, как правило, представляет собой блок. Приведенный ниже метод заменяет каждый элемент числового массива его модулем, причем доступ к массиву регулируется оператором synchronized:
/** сделать все элементы массива неотрицательными */
public static void abs(int[] values) {
synchronized (values) {
for (int i = 0; i << values.length; i++) {
if (values[i] << 0)
values[i] = - values[i];
}
}
}
Массив values содержит изменяемые элементы. Мы синхронизируем доступ к нему, указывая его в качестве объекта в операторе synchronized. После этого можно выполнять цикл и быть уверенным в том, что массив не будет изменен каким-либо другим фрагментом программы, в котором тоже установлена синхронизация для массива values.
От вас не требуется, чтобы объект, указанный как аргумент оператора synchronized, использовался в теле оператора. Можно представить себе ситуацию, при которой единственное назначение объекта заключается в том, чтобы служить для блокировки большего набора объектов. В этом случае объект-представитель может и не обладать собственными функциями, но использоваться во всех операторах synchronized, желающих выполнить действия с некоторыми или всеми объектами из этого набора.
В подобных ситуациях существует и другой подход — спроектировать класс-представитель с несколькими синхронизированными методами, служащими для выполнения операций с другими объектами. При таком варианте не только достигается более четкая инкапсуляция операций, но и исчезает возможный источник ошибок — доступ к объектам вне операторов synchronized, вызванный забывчивостью программиста. Тем не менее иногда с защищаемыми объектами выполняется слишком много операций, чтобы их все можно было инкапсулировать в виде методов класса, и для защиты многопоточного доступа приходится пользоваться оператором synchronized.
Иногда разработчик класса не принимает во внимание его возможное использование в многопоточной среде и не синхронизирует никакие методы. Чтобы применить такой класс в многопоточной среде, у вас имеется две возможности:
· Создать расширенный класс, в котором вы переопределяете нужные методы, объявляете их synchronized и перенаправляете вызовы этих методов при помощи ссылки super.
· Воспользоваться оператором synchronized для обеспечения доступа к объекту, с которым могут работать несколько потоков.
В общем случае расширение класса является более удачным решением — оно устраняет последствия возможной ошибки программиста, забывающего внести доступ к объекту в оператор synchronized. Тем не менее, если синхронизация необходима лишь в одном-двух фрагментах программы, то оператор synchronized предоставляет более простое решение.
Методы wait и notify
Механизм блокировки решает проблему с наложением потоков, однако хотелось бы, чтобы потоки могли обмениваться информацией друг с другом. Для этого существует два метода: wait и notify. Метод wait позволяет потоку дождаться выполнения определенного условия, а метод notify извещает все ожидающие потоки о наступлении некоторого события.
Методы wait и notify определены в классе Object и наследуются всеми классами. Они, подобно блокировке, относятся к конкретным объектам. При выполнении wait вы ожидаете, что некоторый поток известит (notify) о наступлении события тот самый объект, в котором происходит ожидание.
Существует стандартная конструкция, которой следует пользоваться в работе с wait и notify. Поток, ожидающий события, должен включать что- нибудь похожее на следующий фрагмент:
synchronized void doWhenCondition() {
while (!условие)
wait();
... Действия, выполняемые при выполнении условия ...
}
Здесь следует обратить внимание на несколько аспектов:
· Все действия выполняются внутри синхронизированного метода. Это необходимо — в противном случае нельзя быть уверенным в содержимом объекта. Например, если метод не синхронизирован, то после выполнения оператора while нет гарантии, что условие окажется истинным — ситуация могла быть изменена другим потоком.
· Одно из важных свойств определения wait заключается в том, что во время приостановки потока происходит атомарное (atomic) снятие блокировки с объекта. Когда говорят об атомарной приостановке потока и снятии блокировки, имеется в виду, что эти операции происходят вместе и не могут отделяться друг от друга. В противном случае снова возникла бы “гонка”: извещение могло бы придти после снятия блокировки, но перед приостановкой потока. В этом случае извещение никак не влияет на работу потока и фактически теряется. Когда поток возобновляет работу после получения извещения, происходит повторная блокировка.
· Условие всегда должно проверяться внутри цикла. Никогда не следует полагать, что возобновление работы потока означает выполнение условия. Другими словами, не заменяйте while на if.
С другой стороны, метод notify вызывается методами, изменяющими данные, которые могут ожидаться другим потоком.
synchronized void changeCondition() {
... изменить величину, используемую при проверке условия ...
notify();
}
Несколько потоков могут ждать один и тот же объект. Извещение notify возобновляет тот поток, который ждет дольше всех. Если необходимо возобновить все ожидающие потоки, используйте метод notifyAll.
Приводимый ниже класс реализует концепцию очереди. Он содержит методы, которые используются для помещения элементов в очередь и их удаления:
class Queue {
// первый и последний элементы очереди
Element head, tail;
public synchronized void append(Element p) {
if (tail == null)
head = p;
else
tail.next = p;
p.next = null;
tail = p;
notify(); // сообщить ожидающим потокам о новом элементе
}
public synchronized Element get() {
try {
while(head == null)
wait(); // ожидать появления элемента
} catch (InterruptedException e) {
return;
}
Element p = head; // запомнить первый элемент
head = head.next; // удалить его из очереди
if (head == null) // проверить, не пуста ли очередь
tail = null;
return p;
}
}
Такая реализация очереди во многом напоминает ее воплощение в однопоточной системе. Отличий не так уж много: методы синхронизированы; при занесении нового элемента в очередь происходит извещение ожидающих потоков; вместо того чтобы возвращать null для пустой очереди, метод get ждет, пока какой-нибудь другой поток занесет элемент в очередь. Как занесение, так и извлечение элементов очереди может осуществляться несколькими потоками (а не обязательно одним).
Подробности, касающиеся wait и notify
Существует три формы wait и две формы notify. Все они входят в класс Object и выполняются для текущего потока:
public final void wait(long timeout)
Выполнение текущего потока приостанавливается до получения извещения или до истечения заданного интервала времени timeout. Значение timeout задается в миллисекундах. Если оно равно нулю, то ожидание не прерывается по тайм-ауту, а продолжается до получения извещения.
public final void wait(long timeout, int nanos)
Аналог предыдущего метода с возможностью более точного контроля времени; интервал тайм-аута представляет собой сумму двух параметров: timeout (в миллисекундах) и nanos (в наносекундах, значение в диапазоне 0–999999).
public final void wait()
Эквивалентно wait(0).
public final void notify()
Посылает извещение ровно одному потоку, ожидающему выполнения некоторого условия. Потоки, которые возобновляются лишь после выполнения данного условия, могут вызвать одну из разновидностей wait. При этом выбрать извещаемый поток невозможно, поэтому данная форма notify используется лишь в тех случаях, когда вы точно знаете, какие потоки ожидают событий, какие это события и сколько длится ожидание. Если вы не уверены в каком-либо из этих факторов, вероятно, следует воспользоваться методом notifyAll.
public final void notifyAll()
Посылает извещения всем потокам, ожидающим выполнения некоторого условия. Обычно потоки стоят, пока какой-то другой поток не изменит некоторое условие. Используя этот метод, управляющий условием поток извещает все ожидающие потоки об изменении условия. Потоки, которые возобновляются лишь после выполнения данного условия, могут вызывать одну из разновидностей wait.
Все эти методы реализованы в классе Object. Тем не менее они могут вызываться только из синхронизированных фрагментов, с использованием блокировки объекта, в котором они применяются. Вызов может осуществляться или непосредственно из такого фрагмента, или косвенно — из метода, вызываемого в фрагменте. Любая попытка обращения к этим методам для объектов за пределами синхронизированных фрагментов, для которых действует блокировка, приведет к возбуждению исключения IllegalMonitorState Exception.
Планирование потоков
Java может работать как на однопроцессорных, так и на многопроцессорных компьютерах, в однопоточных и многопоточных системах, так что в отношении потоков даются лишь общие гарантии. Вы можете быть уверены в том, что исполнимый (runnable) поток с наивысшим приоритетом будет работать и что все потоки с тем же приоритетом получат некоторую долю процессорного времени. Функционирование потоков с низшим приоритетом гарантируется лишь в случае блокировки всех потоков с высшим приоритетом. /Читателю следует отличать блокировку объекта (lock), о которой говорилось выше, от блокировки потока (block). Терминология, сложившаяся в отечественной литературе, может стать источником недоразумений. - Примеч. перев./ На самом деле не исключено, что потоки с низшим приоритетом будут работать и без таких решительных мер, но полагаться на это нельзя.
Поток называется заблокированным, если он приостановлен или выполняет заблокированную функцию (системную или функцию потока). В случае блокировки потока Java выбирает исполнимый поток с наивысшим приоритетом (или один из таких потоков, если их несколько) и начинает его выполнение.
Runtime-система Java может приостановить поток с наивысшим приоритетом, чтобы дать поработать потоку с тем же приоритетом, — это означает, что все потоки, обладающие наивысшим приоритетом, со временем выполняются. Тем не менее это вряд ли можно считать серьезной гарантией, поскольку “со временем” — понятие растяжимое. Приоритетами следует пользоваться лишь для того, чтобы повлиять на политику планирования для повышения эффективности. Не стоит полагаться на приоритет потоков, если от этого зависит правильность работы алгоритма.
Начальный приоритет потока совпадает с приоритетом того потока, который создал его. Для установки приоритета используется метод setPriority с аргументом, значение которого лежит между константами MIN_PRIORITY и MAX_PRIORITY класса Thread. Стандартный приоритет для потока по умолчанию равен NORM_PRIORITY. Приоритет выполняемого потока может быть изменен в любой момент. Если потоку будет присвоен приоритет ниже текущего, то система может запустить другой поток, так как исходный поток может уже не обладать наивысшим приоритетом. Метод getPriority возвращает приоритет потока.
В общем случае постоянно работающая часть вашего приложения должна обладать более низким приоритетом, чем поток, занятый обработкой более редких событий — например, ввода информации пользователем. Скажем, когда пользователь нажимает кнопку с надписью STOP, он ждет, что приложение немедленно остановится. Если обновление изображения и ввод информации осуществляются с одинаковым приоритетом и во время нажатия кнопки происходит вывод, на то, чтобы поток ввода смог среагировать на нажатие кнопки, может потребоваться некоторое время. Даже несмотря на то, что поток вывода обладает более низким приоритетом, он все равно будет выполняться большую часть времени, поскольку поток пользовательского интерфейса будет заблокирован в ожидании ввода. С появлением введенной информации поток пользовательского интерфейса заставит поток вывода среагировать на запрос пользователя. По этой причине приоритет потока, который должен выполняться постоянно, устанавливается равным MIN_PRIORITY, чтобы он не поглощал все доступное процессорное время.
Несколько методов класса Thread управляют планировкой потоков в системе:
public static void sleep(long millis)
Приостанавливает работу текущего потока как минимум на указанное число миллисекунд. “Как минимум” означает, что не существует гарантий возобновления работы потока точно в указанное время. На время возобновления может повлиять планировка потоков в системе, гранулярность и точность системных часов, а также ряд других факторов.
public static void sleep(long millis, int nanos)
Приостанавливает работу текущего потока как минимум на указанное число миллисекунд и дополнительное число наносекунд. Значение интервала в наносекундах лежит в диапазоне 0–999999.
public static void yield()
Текущий поток передает управление, чтобы дать возможность работать и другим исполняемым потокам. Планировщик потоков выбирает новый поток среди исполняемых потоков в системе. При этом может быть вызван поток, только что уступивший управление, если его приоритет окажется самым высоким.
Приведенный ниже пример демонстрирует работу yield. Приложение получает список слов и создает потоки, предназначенные для вывода отдельного слова в списке. Первый параметр приложения определяет, должен ли каждый поток передавать управление после каждого вызова println; значение второго параметра равно количеству повторений слова при выводе. Остальные параметры представляют собой слова, входящие в список:
class Babble extends Thread {
static boolean doYield; // передавать управление другим потокам?
Static int howOften; // количеств повторов при выводе
String word; // слово
Babble(String whatToSay) {
word = whatToSay;
}
public void run() {
for (int i = 0; i << howOften; i++) {
System.out.println(word);
if (doYield)
yield(); // передать управление другому потоку
}
}
public static void main(String[] args) {
howOften = Integer.parseInt(args[1]);
doYield = new Boolean(args[0]).booleanValue();
// создать поток для каждого слова и присвоить ему
// максимальный приоритет
Thread cur = currentThread();
cur.setPriority(Thread.MAX_PRIORITY);
for (int i = 2; i << args.length; i++)
new babble(args[i]).start();
}
}
Когда потоки работают, не передавая управления друг другу, им отводятся большие кванты времени — обычно этого бывает достаточно, чтобы закончить вывод в монопольном режиме. Например, при запуске программы с присвоением doYield значения false:
Babble false 2 Did DidNot
результат будет выглядеть следующим образом:
Did
Did
DidNot
DidNot
Если же каждый поток передает управление после очередного println, то другие потоки также получат возможность работать. Если присвоить doYield значение true:
Babble true 2 Did DidNot
то остальные потоки также смогут выполняться между очередными выводами и, в свою очередь, будут уступать управление, что приведет к следующему:
Did
DidNot
Did
DidNot
Приведенные выше результаты являются приблизительными. При другой реализации потоков они могут быть другими, хотя даже при одинаковой реализации разные запуски программы могут дать разные результаты. Однако при любой реализации вызов yield повышает шансы других потоков в споре за процессорное время.
Приостановка потоков
Поток может быть приостановлен (suspended), если необходимо быть уверенным в том, что он возобновится лишь с вашего разрешения. Для примера допустим, что пользователь нажал кнопку CANCEL во время выполнения длительной операции. Работу следует приостановить до того момента, когда пользователь подтвердит (или нет) свое решение. Фрагмент программы может выглядеть следующим образом:
Thread spinner; //поток, выполняющий обработку
public void userHitCancel() {
spinner.suspend(); // приостановка
if (askYesNo("Really Cancel?"))
spinner.stop(); // прекращение операции
else
spinner.resume(); // передумал!
}
Метод userHitCancel сначала вызывает suspend для потока, выполняющего операцию, чтобы остановить его вплоть до вашего распоряжения. Затем пользователь должен ответить, действительно ли он хочет отменить операцию. Если да, то метод stop снимает поток; в противном случае метод resume возобновляет работу потока.
Приостановка ранее остановленного потока, а также возобновление работы потока, который не был приостановлен, не приводит ни к каким нежелательным последствиям.
Прерывание потока
В некоторых методах класса Thread упоминается прерывание (interrupting) потока. Соответствующие методы зарезервированы для возможности, которая вскоре будет включена в Java. На момент написания этой книги они еще не полностью реализованы; попытка их вызова приводит к возбуждению исключения NoSuchMethodError и уничтожению вызывающего потока. Вполне возможно, что к тому моменту, когда вы будете читать эту книгу, эти методы уже будут реализованы. В данном разделе приводится их краткий обзор.
Концепция “прерывания” оказывается полезной, если выполняемому потоку необходимо предоставить некоторую степень контроля над моментом обработки события. Например, в цикле вывода может понадобиться информация из базы данных, извлекаемая посредством транзакции; если при этом поступает запрос на прекращение работы, желательно дождаться нормального завершения транзакции. Поток пользовательского интерфейса может реализовать такой запрос, прерывая поток вывода и давая ему возможность дождаться конца транзакции. Подобная схема будет хорошо работать лишь в том случае, если поток вывода “хорошо себя ведет” и в конце каждой транзакции проверяет, не поступил ли запрос на прерывание (и прекращает работу в этом случае).
Прерывание потока в общем случае не должно влиять на его работу, однако некоторые методы (такие, как sleep или wait) возбуждают исключение InterruptedException. Если в вашем потоке во время прерывания выполнялся один из таких методов, то будет возбуждено прерывание Interrupted Exception.
Для работы с прерываниями используются несколько методов. Метод interrupt посылает прерывание в поток; метод isInterrupted проверяет факт прерывания потока; статический метод interrupted проверяет, прерывался ли текущий поток.
Завершение работы потока
Работа потока прекращается, когда происходит выход из его метода run. Так происходит нормальное завершение потока, но вы можете остановить поток и по-другому.
Желательно использовать самый “чистый” способ, который, однако, требует некоторой работы со стороны программиста: вместо того чтобы насильственно прекращать существование потока, лучше дать ему завершиться добровольно. Чаще всего для этого используют логическую переменную, значение которой опрашивается потоком
Самый прямолинейный способ завершить поток — вызвать его метод stop, который запустит объект ThreadDeath, указав ему в качестве цели нужный поток. ThreadDeath является подклассом класса Error, а не Exception (объяснение того, почему так было сделано, приводится в приложении Б). Программистам не следует перехватывать ThreadDeath, если только они не должны выполнить какие-нибудь чрезвычайно неординарные завершающие действия, с которыми не справится finally. Если уж вы перехватываете ThreadDeath, обязательно возбудите объект-исключение заново, чтобы поток мог “умереть”. Если же ThreadDeath не перехватывается, то обработчик ошибок верхнего уровня просто уничтожает поток, не выводя никаких сообщений.
Поток также может возбудить ThreadDeath для самого себя, чтобы завершить свою собственную работу. Это может пригодиться, если поток углубился на несколько уровней ниже метода run и вам не удается легко сообщить run о том, что пора заканчивать.
Другой форме метода stop можно вместо ThreadDeath передать какое-то другое исключение. Хотя обычно возбуждение исключений оказывается не самым лучшим способом для обмена информацией между потоками, вы можете использовать эту форму общения для того, чтобы послать потоку какое-то сообщение. Например, если некоторый поток выполняет длительные вычисления для определенных входных значений, то интерфейсный поток может разрешить пользователю изменить эти значения прямо во время вычислений. Конечно, вы можете просто завершить поток и начать новый. Тем не менее, если промежуточные результаты вычислений могут использоваться повторно, то вместо завершения потока можно создать новый тип исключения Restart Calculation и воспользоваться методом stop, чтобы запустить новое исключение в поток. При этом поток должен перехватить исключение, рассмотреть новые входные значения, по возможности сохранить результаты и возобновить вычисления.
Один поток может ожидать завершения другого потока. Для этого применяется один из методов join. Простейшая форма этого метода ждет завершения определенного потока:
class CalcThread extends Thread {
private double Result;
public void run() {
Result = calculate();
}
public double result() {
return Result;
}
public double calculate() {
// ...
}
}
class join {
public static void main(String[] args) {
CalcThread calc = new CalcThread();
calc.start();
doSomethingElse();
try {
calc.join();
System.out.println("result is "
+ calc.result());
} catch (InterruptedException e) {
System.out.println("No answer: interrupted");
}
}
}
Сначала создается новый тип потока, CalcThread, выполняющий некоторые вычисления. Мы запускаем поток, некоторое время занимаемся другими делами, после чего пытаемся присоединиться (join) к потоку. На выходе из join можно быть уверенным, что метод CalcThread.run завершился, а значение Result получено. Это сработает независимо от того, окончился ли поток CalcThread до doSomethingElse или нет. Когда поток завершается, его объект никуда не исчезает, так что вы можете к нему обращаться.
При вызове других форм join им передаются интервалы тайм-аута, подобные тем, какие используются для метода sleep. Имеются три формы join:
public final void join()
Ожидает безусловного завершения потока, для которого вызывается метод.
public final synchronized void join(long millis)
Ожидает завершения потока или истечения заданного числа миллисекунд (в зависимости от того, что произойдет раньше). Аргумент, равный нулю, означает ожидание без тайм-аута.
public final synchronized void join(long millis, int nanos)
Ожидает завершения потока или тайм-аута с более точным контролем времени. Суммарное время тайм-аута, равное 0 наносекунд, снова означает ожидание без тайм-аута. Количество наносекунд находится в диапазоне 0–999999.
Вызов метода destroy для потока — самая решительная мера. Этот метод уничтожает поток без выполнения нормальных завершающих действий, к которым относится и снятие блокировки со всех объектов потока, так что применение destroy может навечно заблокировать другие потоки. По возможности старайтесь избегать вызова destroy.
Использование Runnable
В интерфейсе Runnable абстрагируется концепция некой сущности, выполняющей программу во время своей активности. Интерфейс Runnable объявляет всего один метод:
public void run();
Класс Thread реализует интерфейс Runnable, поскольку поток как раз и является такой сущностью — во время его активности выполняется программа. Мы уже видели, что для осуществления каких-то особых вычислений можно расширить класс Thread, однако во многих случаях это не слишком просто. Прежде всего, расширение классов производится на основе одиночного наследования — если некоторый класс расширяется для того, чтобы он мог выполняться в потоке, то одновременно расширить и его, и Thread не удастся. Кроме того, если вам нужна только возможность выполнения, то вряд ли вы захотите наследовать и все накладные расходы, связанные с Thread.
Во многих случаях проще реализовать Runnable. Объект Runnable может выполняться в отдельном потоке — для этого следует передать его конструктору Thread. Если объект Thread конструируется с объектом Runnable, то реализация Thread.run вызывает метод run переданного объекта.
Приведем версию класса PingPong, в которой используется интерфейс Runnable. Сравнение этих двух версий показывает, что они выглядят почти одинаково. Наиболее существенные отличия заключаются в супертипе (Runnable вместо Thread) и методе main:
class RunPingPong inplements Runnable {
String word; // выводимое слово
int delay; // длительность паузы
PingPong(String whatToSay, int delayTime) {
word = whatToSay;
delay = delayTime;
}
public void run() {
try {
for (;;) {
System.out.print(word + " ");
Thread.sleep(delay); // подождать следующего
// вывода
}
} catch (InterruptedException e) {
return; // завершить поток
}
}
public static void main(String[] args) {
Runnable ping = new RunPingPong("ping", 33);
Runnable pong = new RunPingPong("PONG", 100);
}
}
Сначала определяется новый класс, реализующий интерфейс Runnable. Код метода run в этом классе совпадает с его реализацией в классе PingPong. В методе main создаются два объекта RunPingPong с разными временными интервалами; затем для каждого из них создается и немедленно запускается новый объект Thread.
Существует четыре конструктора Thread, которым передаются объекты Runnable:
· public Thread(Runnable target)
Конструирует новый объект Thread, использующий метод run указанного класса target.
· public Thread(Runnable target, String name)
Конструирует новый объект Thread с заданным именем name, использующий метод run указанного класса target.
· public Thread(ThreadGroup group, Runnable target)
Конструирует новый объект Thread, входящий в заданную группу ThreadGroup и использующий метод run указанного класса target.
· public Thread(ThreadGroup group, Runnable target, String name)
Конструирует новый объект Thread с заданным именем name, входящий в заданную группу ThreadGroup и использующий метод run указанного класса target.
Ключевое слово volatile
Механизм синхронизации помогает в решении многих проблем, однако, если вы откажетесь от его использования, сразу несколько потоков смогут одновременно изменять значение некоторого поля. Если это делается намеренно (может быть, для синхронизации доступа используются другие средства), следует объявить поле с ключевым словом volatile. Например, если у вас имеется переменная, значение которой постоянно отображается потоком графического вывода и может изменяться несинхронизированными методами, то фрагмент вывода может выглядеть следующим образом:
currentValue = 5;
for (;;) {
display.showValue(currentValue);
Thread.sleep(1000); // подождать 1 секунду
}
Если бы значение currentValue не могло изменяться внутри метода ShowValue, то компилятор мог бы предположить, что величина currentValue остается в цикле постоянной, и просто использовать константу 5 вместо вызова showValue.
Однако, если во время выполнения цикла значение currentValue может быть изменено другим потоком, то предположение компилятора будет неверным. Объявление поля currentValue с ключевым словом volatile не позволяет компилятору делать подобные предположения.
Варианты заданий:
1. Разместить на форме друг под другом несколько кнопок и устроить гонки от левого края формы до правого. Каждая гонка должна ехать со своей скоростью (каждый раз определяющейся случайным образом) и в отдельном потоке. Гонка останавливается, как только какая-то кнопка достигнет «финиша». Финалист меняет цвет фона + появления окна сообщения с именем победителя. Необходимо наличие возможности рестарта гонки с «правильным» завершением соответствующих потоков.
2. Работать игру «теннис» в объеме первых телеприставок. Задача игроков отбивать мяч, перемещая ракетки вверх-вниз. Ракетки находятся в левой и правой части экрана и перемещаются с помощью клавиатуры. Мяч отскакивает от верхней и нижней стенок. Каждая ракетка и мяч должны выполнятся в отдельных потоках. Должен вестись счет, а также иметься кнопка, для начала игры заново (обнуляющая счет и «правильно» завершающая соответствующие потоки).
3. Реализовать «синхронные» методы чтения/записи для переменной типа String. Правила обращения следующие:
a. Записывать в одно время может только один поток, любой следующий «писатель» должен ждать (блокироваться).
b. Если один поток выполняет запись, то любой читатель должен ждать завершения этой записи.
c. Пока происходит чтение, запись невозможна.
d. Однако требуется избежать такой ситуации, когда запись невозможна из-за сменяющих друг друга операций чтения.
e. Любое количество читающих потоков могут читать одновременно