Главная страница

Java. Полное руководство. 8-е издание. С. Н. Тригуб Перевод с английского и редакция


Скачать 25.04 Mb.
НазваниеС. Н. Тригуб Перевод с английского и редакция
АнкорJava. Полное руководство. 8-е издание.pdf
Дата28.02.2017
Размер25.04 Mb.
Формат файлаpdf
Имя файлаJava. Полное руководство. 8-е издание.pdf
ТипДокументы
#3236
страница20 из 90
1   ...   16   17   18   19   20   21   22   23   ...   90
Реализация интерфейса
R u n n a b l Самый простой способ создания потока — это объявление класса, реализующего интерфейс
Runnable. Интерфейс
Runnable абстрагирует единицу исполняемого кода. Вы можете создать поток из любого объекта, реализующего интерфейс
Runnable. Чтобы реализовать интерфейс
Runnable, класс должен объявить единственный метод run ().
public void r u n (Внутри метода run
() вы определяете код, который, собственно, составляет новый поток. Важно понимать, что метод run
() может вызывать другие методы, использовать другие классы, объявлять переменные — точно также, как это делает главный поток. Единственным отличием является то, что метод run
() устанавливает точку входа для другого, параллельного потока внутри вашей программы. Этот поток завершится, когда метод run
() вернет управление.
После того как будет объявлен класс, реализующий интерфейс
Runnable, вы создадите объект типа
Thread из этого класса. В классе
Thread определено несколько конструкторов. Тот, который должен использоваться в данном случае, выглядит следующим образом объект потока

имя_потока)
В этом конструкторе объект_пот ока — это экземпляр класса, реализующего интерфейс
Runnable. Он определяет, где начнется выполнение потока. Имя нового потока передается в параметре имя_потока.
После того как новый поток будет создан, он не запускается до тех пор, пока вы не вызовете метод start ()
, объявленный в классе
Thread. По сути, метод start (
) выполняет вызов метода run
(). Метод start
() показан ниже Рассмотрим пример, создающий новый потоки запускающий его выполнение Создание второго потока NewThread implements Runnable {

Thread t;
NewThread() {
// Создать новый, второй поток = new Thread(this, "Демонстрационный поток");
System.out.println("Дочерний поток создан " + t ) ;
t.start(); // Запустить поток Точка входа второго потока void r u n () {

2 6 Часть I. Язык Java
try {
for(int i = 5; i > 0 ; i--) Дочерний поток " + i);
Thread.sleep(500);
}
} catch (InterruptedException e) {
System.out.p r i Дочерний поток прерван.");
}
System.out.println("Дочерний поток завершен ThreadDemo {
public static void main(String a r g s []) {
new N e w T h readO; // создать новый поток {
for(int i = 5; i > 0 ; i--) Главный поток " + i);
Thread.s l eep(1000);
}
} catch (InterruptedException e) Главный поток прерван.");
}
System.out.println("Главный поток завершен.");
}
}
Внутри конструктора
NewThread
() в следующем операторе создается новый объект класса
Thread.
t = new Thread(this, "Демонстрационный поток");
Передача объекта t h i s в первом аргументе означает, что вы хотите, чтобы новый поток вызвал метод ru n () объекта t h i s . Далее вызывается метод s t a r t (), в результате чего запускается выполнение потока начиная с метода r u n (). Это запускает цикл f o r дочернего потока. После вызова метода s t a r t () конструктор NewThread () возвращает управление методу m ain (). Когда главный поток продолжает свою работу, он входит в свой цикл f o r . После этого оба потока выполняются параллельно, совместно используя ресурсы процессора в одноядерной системе, вплоть до завершения своих циклов. Вывод, создаваемый этой программой, показан ниже (ваш вывод может варьироваться, в зависимости от конкретной среды исполнения).
Дочерний поток Th r e a d Демонстрационный поток,5,main]
Главный поток Дочерний поток Дочерний поток 4 Главный поток 4
Дочерний поток 3 Дочерний поток Главный поток Дочерний поток Дочерний поток завершен.
Главный поток 2 Главный поток 1 Главный поток завершен.
Как уже упоминалось ранее, в многопоточной программе главный поток зачастую должен завершать выполнение последним. Фактически, для некоторых старых виртуальных машин Java (JVM), если главный поток завершается до завершения до
Глава 11. Многопоточное программирование 6 7
черних потоков, то исполняющая система Java может зависнуть. Предыдущая программа гарантирует, что главный поток завершится последним, поскольку главный поток спит 1000 миллисекунд между итерациями цикла, а дочерний поток — только 500 миллисекунд. Это заставляет дочерний поток завершиться раньше главного. Но далее вы узнаете лучший способ ожидания завершения потоков.
Расширение класса
T h r e a Еще один способ создания потока — это объявить класс, расширяющий класса затем создать экземпляр этого класса. Расширяющий класс обязан переопределить метод run ()
, который является точкой входа для нового потока. Он также должен вызвать метод start
() для запуска выполнения нового потока. Ниже приведен пример предыдущей программы, переписанной с использованием расширения класса
Thread.
// Создание второго потока расширением Thread
class NewThread extends Thread {
NewThread() {
// Создать новый второй поток Демонстрационный поток r Дочерний поток " + this);
start(); // Запустить поток Точка входа второго потока
public void r u n () {
try {
for(int i = 5; i > 0; i--) Дочерний поток " + i);
Thread.sleep(500);
}
} catch (InterruptedException e) Дочерний поток прерван.");
}
System.out.println("Дочерний поток завершен ExtendThread {
public static void main(String a r g s []) {
new NewThread(); // Создать новый поток {
for(int i = 5; i > 0; i--) Главный поток " + i);
T hread.s l eep(1000);
}
} catch (InterruptedException e) Главный поток прерван.");
}
System.out.println("Главный поток завершен.");
}
}
Эта программа создает точно такой же вывод, что и предыдущая версия. Как вы можете видеть, дочерний поток создается при конструировании объекта класса
NewThread, который наследуется от класса
Thread.

2 6 Часть I. Язык Обратите внимание на метод super
() внутри класса
NewThread. Он вызывает следующую форму конструктора
Thread ().
public Thre a d (String
имя_потока)
Здесь имя_потока указывает имя потока.
Выбор подхода
В данный момент вы можете спросить, почему Java предлагает два способа создания дочерних потоков и какой из них лучше. Ответы на эти вопросы взаимосвязаны. Класс
Thread определяет несколько методов, которые могут быть переопределены в производных классах. Из этих методов только один должен быть переопределен в обязательном порядке — это метод run ()
. Конечно, этот же метод нужен, когда вы реализуете интерфейс
Runnable. Многие программисты Java считают, что классы следует расширять только в случаях, когда они должны быть усовершенствованы или некоторым образом модифицированы. Поэтому если вы не переопределяете никаких других методов класса
Thread, то вероятно, лучше просто реализовать интерфейс
Runnable. Кроме того, при реализации интерфейса
Runnable ваш класс потока не должен наследовать класс
Thread, чтобы освободиться от наследования других классов. В конечном счете, какой из подходов использовать, остается на ваше усмотрение. Тем не менее далее в этой главе мы будем создавать потоки, используя классы, реализующие интерфейс Создание множества потоков

До сих пор вы использовали только два потока главный и один дочерний. Однако ваша программа может порождать столько потоков, сколько необходимо. Например, в следующей программе создается три дочерних потока Создание множества потоков
class NewThread implements Runnable {
String name; // имя потока
Thread t;
NewThread(String threadname) {
name = threadname;
t = new Thread(this, name);
System.out.p r Новый поток " + t);
t .start(); // запустить поток Входная точка потока
public void r u n () {
try {
for(int i = 5; i > 0; i--) {
System.out.println(name + ": " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println(name + " прерван + " завершен
Глава 11. Многопоточное программирование 6 9
class Mult i T h r e a d D e m o {
public static voi d mai n ( S t r i n g a r g s []) {
ne w N e w T h r e a d (" Один запуск потоков
n ew N e w T h r e a d (" Два Три ожидание завершения других потоков
T h r e a d . s l e e p (10000);
} catch (InterruptedException e) {
S y s t e m . o u t . p r i n t l n (Главный поток прерван y s t e m . o u t . p r i n t l n (Главный поток завершен.");
}
}
Пример вывода этой программы показан ниже (ваш вывод может отличаться, в зависимости от конкретной среды исполнения).
Новый поток T h r e a d [Один,5,main]
Новый поток T h r e a d Два, 5 Новый поток T h r e a d [Три,5,main]
О дин Два 5 Три 5
Один Два 4
Три Один Три Два 3 Один Три Два Один Три Два Один завершен.
Два завершен.
Три завершен.
Главный поток завершен.
Как видите, будучи запущенными, все три дочерних потока совместно используют ресурс центрального процессора. Обратите внимание на вызов метода s l e e p (10000) в методе m a i n (). Это заставляет главный поток уснуть на 10 секунд и гарантирует, что он будет завершен последним.
Использование методов i s A l i v e ( ) и j o i n ( Как упоминалось, зачастую необходимо, чтобы главный поток завершался последним. В предыдущих примерах это обеспечивается вызовом метода s l e e p () из метода m a i n () с задержкой, достаточной для того, чтобы гарантировать, что все дочерние потоки завершатся раньше главного. Однако это неудовлетворительное решение, которое вызывает серьезный вопрос как один поток может знать о том, что другой завершился К счастью, класс T h re a d предлагает средство, которое дает ответ на этот вопрос

2 7 Часть I. Язык Существует два способа определить, что поток был завершен. Во-первых, вы можете вызвать метод i s A l i v e () для этого потока. Этот метод определен в классе T h re a d , и его общая форма такова Boolean Метод i s A l i v e () возвращает значение t r u e , если поток, для которого он вызван, еще выполняется. В противном случае он возвращает значение
false.
Во-вторых, существует метод, который выбудете использовать чаще, чтобы дождаться завершения потока, а именно — метод j o i n ().
final void join() throws Этот метод ожидает завершения потока, для которого он вызван. Его имя отражает концепцию, что вызывающий поток ожидает, когда указанный поток присоединиться к нему. Дополнительные формы метода j o i n () позволяют указывать максимальный период времени, который выбудете ожидать завершения указанного потока.
Ниже приведена усовершенствованная версия предыдущего примера, использующая метод j o i n () для гарантии того, что главный поток завершился последним. Здесь также демонстрируется применение метода i s A l i v e ().
// Применение join() для ожидания завершения потоков
class NewThread implements Runnable {
String name; // имя потока
Thread t;
NewThread(String threadname) {
name = threadname;
t = new Thread(this, Новый поток " + t);
t.start(); // Запуск потока Входная точка потока
public void r u n () {
try {
for(int i = 5; i > 0 ; i--) {
System.out.println(name + ": " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println(name + " прерван + " завершен DemoJoin {
public static void main(String a r g s []) {
NewThread
obi
= new Одни
ob2
= new Два
ob3
= new NewThread("Три");
System.out.println("Поток Один запущен "
+ o b i .t Поток Два запущен "
+ o b 2 .t Поток Три запущено Ь З .t .isAlive());
// ожидать завершения потоков
try {
Глава 11. Многопоточное программирование 7 Ожидание завершения потоков
o b i .t .j o i n ();
o b 2 .t .j o i n ();
o b 3 .t .j o i n ();
} catch (InterruptedException e) Главный поток прерван");
}
System.out.println("Поток Один запущен "
+ o b i .t Поток Два запущен "
+ o b 2 .t Поток Три запущено Ь З .t Главный поток завершен.");
}
}
Пример вывода этой программы показан ниже (ваш вывод может отличаться, в зависимости от конкретной среды исполнения).
Новый поток Th r e a d [Одни,5,main]
Новый поток T h read[Два,5.main]
Новый поток Th r e a d [Три,5,main]
Поток Один запущен true Поток Два запущен true
Поток Три запущен true
Ожидание завершения потоков.
О дин Два 5 Три 5 Один Два 4 Три Один Два 3 Три Один Два Три 2 Один Два 1 Три Два завершен.
Три завершен.
Один завершен.
Поток Один запущен false Поток Два запущен false Поток Три запущен false Главный поток завершен.
Как видите, после того как вызовы метода
j o i n () вернут управление, потоки прекращают работу.
Приоритеты потоков
Планировщик потоков использует приоритеты потоков для принятия решения о том, когда каждому потоку будет разрешено работать. Теоретически высокопри­
оритетные потоки получают больше времени процессора, чем низкоприоритет­

2 7 2 Часть I. Язык Java
ные. Практически объем процессорного времени, который получает поток, часто зависит от нескольких факторов, помимо его приоритета. (Например, то,.как операционная система реализует многозадачность, может влиять на относительную доступность процессорного времени) Высокоприоритетный поток может также выгружать низкоприоритетный. Например, когда низкоприоритетный поток работает, а вы сокоприоритетный собирается продолжить свою прерванную работу в связи с приостановкой или ожиданием завершения операции ввода-вывода), то последний выгружает низкоприоритетный поток.
Теоретически потоки с одинаковым приоритетом должны получать равный доступ к центральному процессору. Новы должны быть осторожны. Помните, что язык Java спроектирован для работы в широком спектре сред. Некоторые из этих сред реализуют многозадачность принципиально отлично от других. В целях безопасности потоки с одинаковым приоритетом должны получать управление в равной степени. Это гарантирует, что все потоки получат возможность выполняться в среде операционных систем сне вытесняющей многозадачностью. На практике, даже в средах сне вытесняющей многозадачностью, большинство потоков все- таки имеет шанс выполняться, поскольку неизбежно сталкиваются сблокирую щими ситуациями, такими как ожидание ввода-вывода. Когда подобное случается, заблокированный поток приостанавливается и остальные потоки могут работать. Но если вы хотите добиться гладкой многопоточной работы, тоне должны полагаться на это. К тому же некоторые типы задач интенсивно нагружают процессор. Такие потоки захватывают процессор. Потокам такого типа следует передавать управление от случая к случаю, чтобы позволить выполняться другим.
Чтобы установить приоритет потока, используйте метод setPriorityf) класса
Thread. Так выглядит его общая форма void setPriority(int

уровень)
Здесь уровень задает новый уровень приоритета для вызывающего потока. Значение уровень должно быть в пределах диапазона от
MIN_PRIORITY до МАХ
PRIORITY. В настоящее время эти значения равны соответственно 1 и 10. Чтобы вернуть потоку приоритет по умолчанию, укажите
NORM_PRIORITY, который в настоящее время равен 5. Эти приоритеты определены как статические финальные
(static final) переменные в классе Вы можете получить текущее значение приоритета потока, вызвав метод get-
PriorityO класса
Thread.
final int Реализации Java могут иметь принципиально разное поведение в том, что касается планирования потоков. Большинство несовпадений возникает, когда вы полагаетесь на вытесняющую многозадачность вместо совместного использования времени процессора. Наиболее безопасный способ получить предсказуемое меж- платформенное поведение Java — это использовать потоки, которые принудительно осуществляют управление центральным процессором.
Синхронизация
Когда два или более потоков имеют доступ к одному совместно используемому ресурсу, они нуждаются в гарантии, что ресурс будет использован только одним потоком водно и тоже время. Процесс обеспечения этого называется синхронизацией Как вы увидите, язык Java предлагает ее уникальную поддержку на уровне языка
Глава 11. Многопоточное программирование 7 Ключом к синхронизации является концепция монитора. Монитор — это объект, который используется, как взаимоисключающая блокировка (mutually exclusive lock — m utex), или мьютекс. Только один поток может водно и тоже время владеть монитором. Когда поток запрашивает блокировку, говорят, что он входит в монитор. Все другие потоки, которые пытаются войти в заблокированный монитор, будут приостановлены до тех пор, пока первый поток не выйдет из монитора. Обо всех прочих потоках говорят, что они ожидают монитора. Поток, который владеет монитором, может повторно войти в него, если пожелает.
Вы можете синхронизировать ваш код двумя способами, которые предусматривают использование ключевого слова synchronized; здесь рассмотрим оба способа.
Использование синхронизированных методов
Синхронизация в Java проста, поскольку объекты имеют собственные, ассоциированные сними неявные мониторы. Чтобы войти в монитор объекта, следует просто вызвать метод, модифицированный ключевым словом synchronized. Когда поток находится внутри синхронизированного метода, все другие потоки, которые пытаются вызвать его (или любые другие синхронизированные методы) в том же экземпляре, должны ожидать. Чтобы выйти из монитора и передать управление объектом другому ожидающему потоку, владелец монитора просто возвращает управление из синхронизированного метода.
Чтобы понять необходимость синхронизации, давайте начнем с простого примера, который не использует ее, хотя и должен. Следующая программа содержит три простых класса. Первый из них, Ca l lm e, имеет единственный метод —c a l l (). Этот метод принимает параметр msg класса S t r i n g и пытается вывести строку msg внутри квадратных скобок. Интересно отметить, что после того, как метод c a l l () выводит открывающую скобку и строку msg, он вызывает метод T h r e a d . s l e e p ( l O O O ) , который приостанавливает текущий поток на одну секунду.
Конструктор следующего класса,
Caller, принимает ссылку на экземпляры классов
Callme и
String, которые сохраняются соответственно в переменных target и msg. Конструктор также создает новый поток, который вызовет метод run
() объекта. Поток стартует немедленно. Метод run
() класса
Caller вызывает метод call
() для экземпляра target класса
Callme, передавая ему строку msg. Наконец, класс
Synch начинает с создания единственного экземпляра класса
Callme и трех экземпляров класса
Caller, каждый с уникальной строкой сообщения. Один экземпляр класса
Callme передается каждому конструктору
Caller ().
// Эта программа не синхронизирована
class Callme {
void call(String msg) {
System.out.print("[" + msg);
try {
Thread.sleep(1000);
} c a t c h (InterruptedException e) Прервано Caller implements Runnable {
String msg;
Callme target;
Thread t ;

2 7 4 Часть I. Язык Java
public Caller(C al lm e targ, String s) {
target = targ;
msg = s;
t = ne w T h r e a d ( t h i s );
t .
s t a r t ();
}
public voi d r u n () {
t a r g e t .c a l l ( m s g ) ;
}
}
class Synch {
public static voi d m ai n (S tri ng a r g s []) {
Callme target = ne w C a l l m e O ;
Caller obi
= ne w C a l l e r (t a r g e t ,
"Добро пожаловать a r g e t ,
"в синхронизированный ob3 = ne w C a l l e r (t a r g e t ,
"мир ожидание завершения потока
try {
o b i .t .j o i n () ;
o b 2 .t .j o i n ();
o b 3 .t .j o i n ();
} c a t c h (I nt er rup ted Exc ept ion e) {
S y s t e m . o u t . p r i n t l n Прервано" Вот вывод этой программы.
Добро пожаловать в синхронизированный мир Как видите, вызывая метод s l e e p (), метод c a l l () позволяет переключиться на выполнение другого потока. Это приводит к смешанному выводу трех строк сообщений. В этой программе нет ничего, что предотвращает вызов потоками одного итого же метода водном и том же объекте водно и тоже время. Это называется состоянием гопак (race condition) или конфликтом, поскольку три потока соревнуются друг с другом в окончании выполнения метода. Этот пример использует метод s l e e p (), чтобы сделать эффект повторяемыми наглядным. В большинстве ситуаций этот эффект менее заметен и менее предсказуем, поскольку вы не можете предвидеть, когда произойдет переключение контекста. Это может привести к тому, что программа один раз отработает правильно, а другой раз — нет.
Чтобы исправить эту программу, следует сериализироватъ доступ к методу c a l 1 (). То есть водно и тоже время вы должны разрешить доступ к этому методу только одному потоку. Чтобы сделать это, вам нужно просто предварить объявление метода c a l l () ключевым словом s y n c h r o n i z e d , как показано ниже Callme {

syn chronized void call (St rin g msg) Это предотвратит доступ другим потокам к методу c a l l (), когда один из них уже использует его. После того как слово s y n c h r o n i z e d добавлено к методу c a l l (), результат работы программы будет выглядеть следующим образом.
[Добро пожаловать]
[в синхронизированный]
[м и р !]
Глава 11. Многопоточное программирование 7 Всякий раз, когда у вас есть метод или группа методов, которые манипулируют внутренним состоянием объекта в многопоточной среде, следует использовать ключевое слово s y n c h r o n i z e d , чтобы исключить ситуацию с гонками. Помните, что как только поток входит в любой синхронизированный метод экземпляра, ни один другой поток не может войти нив один синхронизированный метод того же экземпляра. Однако несинхронизированные методы экземпляра по-прежнему остаются доступными для вызова.
Оператор
s y n c h r o n i z e Хотя создание синхронизированных методов в ваших классах — простой и эффективный способ синхронизации, все же он работает не во всех случаях. Чтобы понять, почему, рассмотрим следующее. Предположим, что вы хотите синхронизировать доступ к объектам классов, которые небыли предназначены для много­
поточного доступа. То есть класс не использует синхронизированных методов. Более того, класс был написан не вами, а независимым разработчиком, и у вас нет доступа к его исходному коду. Значит, вы не можете добавить слово s y n c h r o ­
n i z e d к объявлению соответствующих методов класса. Как может быть синхронизирован доступ к объектам такого класса К счастью, существует довольно простое решение этой проблемы вы просто заключаете вызовы методов этого класса в блок s y n c h r o n i z e d Вот общая форма оператора s y n c h r o n i z e d .
synchroni
z e d (объект) {
// операторы, подлежащие синхронизации
}
Здесь объект —
это ссылка на синхронизируемый объект. Блок s y n c h r o n i z e d гарантирует, что вызов метода объекта произойдет только тогда, когда текущий поток успешно войдет в монитор объекта.
Ниже показана альтернативная версия предыдущего примера с использованием синхронизированного блока внутри метода r u n ().
// Эта программа использует синхронизированный блок
class Callme {
void call(String msg) {
System.out.print("[" + msg);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println("]");
}
}
class Caller implements Runnable {
String msg;
Callme target;
Thread t;
public Caller(Callme targ, String s) {
target = targ;
msg = s;
t = new Thread(this);
t .start();
}

2 7 6 Часть I. Язык Java
// синхронизированные вызовы call()
public void r u n () {
synchronized(target)
{ // синхронизированный блок Synchl {
public static void main(String a r g s []) {
Callme target = new C a l l m e O ;
Caller obi = new Call e r (target,
"Добро пожаловать ob2 = new Call e r (target,
"в синхронизированный ob3 = new C a ller(target,
"мир ожидание завершения потока
try {
o b i .t .j o i n () ;
o b 2 .t .j o i n ();
o b 3 .t .j o i n () ;
} c a t c h (InterruptedException e) {
System.out.println("Прервано");
}
}
}
Здесь метод call
() не модифицирован словом synchronized. Вместо этого используется оператор synchronized внутри метода run
() класса
Caller. Это позволяет получить тот же корректный результат, что ив предыдущем примере, поскольку каждый поток ожидает окончания выполнения своего предшественника.
Межпотоковые коммуникации
Предыдущие примеры, безусловно, блокировали другие потоки от асинхронного доступа к некоторым методам. Это использование неявных мониторов объектов Java является мощным средством, новы можете достичь более подробного уровня контроля за счет межпроцессных коммуникаций. Как вы увидите, это особенно просто в Как обсуждалось ранее, многопоточность заменила программирование на основе циклов событий за счет разделения задач на дискретные, логически обособленные единицы. Потоки предоставляют также второе преимущество они исключают опрос. Опрос обычно реализуется в виде цикла, используемого для периодической проверки некоторого условия. Как только условие истинно, выполняется определенное действие. Это расходует время процессора. Например, рассмотрим классическую проблему, когда один поток создает некоторые данные, а другой принимает их. Чтобы сделать проблему интересней, предположим, что поставщик данных должен ожидать, когда потребитель завершит работу, прежде чем поставщик создаст новые данные. В системах с опросом потребитель данных тратит много циклов процессора на ожидание данных от поставщика. Как только поставщик завершает работу, он должен начать опрос, расходующий циклы процессора в ожидании завершения работы потребителя данных, и т.д. Понятно, что такая ситуация нежелательна.
Чтобы избежать опроса, Java включает элегантный механизм межпроцессных коммуникаций с использованием методов wait (), notify (
) и notifyAll (). Эти методы реализованы как финальные в классе
Obj ect, поэтому они доступны
Глава 11. Многопоточное программирование
2 7 всем классам. Все три метода могут быть вызваны только из синхронизированного контекста. Хотя сточки зрения компьютерной науки они концептуально сложны, правила применения этих методов достаточно просты.
• Метод wait
() принуждает вызывающий поток отдать монитор и приостановить выполнение до тех пор, пока какой-нибудь другой поток не войдет в тот же монитор и не вызовет метод not i fy (Метод notify
() возобновляет работу потока, который вызвал метод wait
() в том же объекте.
• Метод notifyAll
() возобновляет работу всех потоков, которые вызвали метод wait
() в том же объекте. Одному из потоков дается доступ.
Эти методы объявлены в классе
Obj ect, как показано ниже void wait() throws InterruptedException

final void n o t i f y ()
final void Существуют дополнительные формы метода wait ()
, позволяющие указать время ожидания.
Прежде чем рассматривать пример, демонстрирующий межпотоковое взаимодействие, необходимо сделать одно важное замечание. Хотя метод w ai t () обычно ожидает до тех пор, пока не будет вызван метод n o t i f y () или n o t i f y A l l (), существует вероятность, что в очень редких случаях ожидающий поток может быть возобновлен поддельным сигналом. При этом ожидающий поток возобновляется без вызова метода n o t i f y () или n o t i f y A l l (). (По сути, поток возобновляется без явных причин) Из-за этой маловероятной возможности Oracle рекомендует выполнять вызовы метода w a i t () внутри цикла, проверяющего условие, по которому поток ожидает. В приведенном ниже примере показан такой подход.
А пока рассмотрим пример, использующий методы wait
() и notify
(). Для начала проанализируем следующий простой пример программы, некорректно реализующий задачу поставщик потребитель. Она состоит из четырех классов
Q
— очередь, которую нужно синхронизировать,
Producer
— объект-поток, который создает элементы очереди,
Consumer
— объект-поток, принимающий элементы очереди, и
PC
— крошечный класс, который создает объекты классов
Q,
Producer и
Consumer.
// Неправильная реализация поставщика и потребителя
class Q {
int n;
synchronized int g e t () Получено " + n ) ;
return n;
}
synchronized void put(int n) {
this.n = Отправлено " + n ) ;
}
}
class Producer implements Runnable {
Q q;
Producer(Q q) {
this.q = q;

2 7 8 Часть I. Язык Java
new Thread(this, "Поставщик void r u n () {
int i = 0;
while(true) {
q.put(i
+
+) ;
}
}
}
class Consumer implements Runnable {
Q q;
Consumer(Q q) {
this.q = q;
new Thread(this, "Потребитель r t ();
}
public void r u n () {
while(true) { q.
get () ;
}
}
class PC {
public static void main(String a r g s []) {
Q q = new Q ();
new Producer(q);
new Consumer(q);
System.out.p r Для останова нажмите Несмотря на то что методы p u t () ив классе Q синхронизированы, ничто не остановит переполнение потребителя поставщиком, как и ничто не помешает потребителю извлечь один и тот же компонент очереди дважды. То есть вы получите неверный результат, показанный ниже (точная последовательность может быть другой, в зависимости от скорости процессора и загрузки).
Отправлено:
1 Получено 1
Получено 1
Получено 1 Получено 1 Получено 1 Отправлено 2 Отправлено 3 Отправлено 4 Отправлено 5 Отправлено 6 Отправлено 7 Получено Как видите, после того, как поставщик отправляет 1, запускается потребитель и получает это же значение 1 пять раз подряд. Затем поставщик продолжает работу и поставляет значения от 2 доне давая возможности потребителю получить их
Глава 11. Многопоточное программирование 7 Правильный способ написания этой программы на языке Java заключается в том, чтобы применить методы w a i t () и n o t i f y () для передачи сигналов в обоих направлениях Правильная реализация поставщика и потребителя
class Q {
int n;
boolean valueSet = false;
synchronized int g e t () {
w h i l e (!valueSet)
try {
wait () ;
} c a t c h (InterruptedException e) {
System.out.println("InterruptedException перехвачено");
}
System.out.println("Получено:
" + n) ;
valueSet = false;
noti f y ();
return n;
}
synchronized void put(int n) {
while(valueSet)
try {
w a i t ();
} c a t c h (InterruptedException e) {
System.out.println("InterruptedException перехвачено = n;
valueSet = Отправлено " + n ) ;
not i f y ();
}
}
class Producer implements Runnable {
Q q;
Producer(Q q) {
this.q = q;
new Thread(this, "Поставщик) ;
}
public void run() {
int i = 0;
while(true) {
q . p u t (i++);
}
}
}
class Consumer implements Runnable {
Q q;
Consumer(Q q) {
this.q = q;
new Thread(this, "Потребитель r t ();

2 8 Часть I. Язык Java
}
public void r u n () {
while(true) {
q. g e t ();
}
}
}
class PCFixed {
public static void main(String a r g s []) {
Q q = new Q ();
new Producer(q);
new Consumer(q);
System.out.p r i Для останова нажмите Внутри метода get
() вызывается метод wait
(). Это приостанавливает работу потока до тех пор, пока объект класса
Producer не известит вас о том, что данные прочитаны. Когда это происходит, выполнение внутри метода get
() продолжается. После получения данных метод get (
) вызывает методу. Это сообщает объекту класса
Producer, что все в порядке и можно помещать в очередь следующий элемент данных. Внутри метода put
() метод wait
() приостанавливает выполнение до тех пор, пока объект класса
Consumer не извлечет элемент из очереди. Когда выполнение возобновится, следующий элемент данных помещается в очередь и вызывается метод notify ()
. Это сообщает объекту класса
Consumer, что он теперь может извлечь его.
Ниже приведен вывод программы, который доказывает, что теперь синхронизация работает корректно.
Отправлено:
1 Получено 1
Отправлено 2 Получено 2 Отправлено 3 Получено 3 Отправлено 4 Получено 4 Отправлено 5 Получено Взаимная блокировка
Следует избегать особого типа ошибок, имеющего отношение к многозадачности. Это — взаимная блокировка (deadlock), которая происходит, когда потоки имеют циклическую зависимость от пары синхронизированных объектов. Предположим, что один поток входит в монитор объекта X, а другой — в монитор объекта
Y. Если поток в объекте
X попытается вызвать любой синхронизированный метод объекта
Y, он будет блокирован, как и ожидалось. Однако если поток объекта
Y, в свою очередь, попытается вызвать любой синхронизированный метод объекта
X, то поток будет ожидать вечно, поскольку для получения доступа к объекту
X он должен снять свой собственный блок с объекта
Y, чтобы первый поток мог работать. Взаимная блокировка является ошибкой, которую трудно отладить, по двум следующим причинам
Глава 11. Многопоточное программирование 8 В общем, она случается довольно редко, когда выполнение двух потоков точно совпадает по времени.
• Она может происходить, когда в этом участвует более двух потоков и двух синхронизированных объектов. (То есть взаимная блокировка может случиться в результате более сложной последовательности событий, чем в приведенном примере.)
Чтобы полностью разобраться с этим явлением, лучше рассмотреть его в действии. Следующий пример создает два класса Аи В с методами f оо () и bar () соответственно, которые приостанавливаются непосредственно перед попыткой вызова метода другого класса. Главный класс, названный
Deadlock, создает экземпляры классов Аи В, а затем запускает второй поток, устанавливающий состояние взаимной блокировки. Методы f оо
() и bar
() используют метод sleep
(), чтобы стимулировать появление взаимной блокировки Пример взаимной блокировки
class А {
synchronized void foo(B b) {
String name = Thread.currentThread().getName();
System.out.println(name + " вошел в A.foo");
try {
Thread.sleep(1000);
} catch(Exception e) А прерван, out .print In (name + " пытается вызвать B.lastO");
b .last () ;
}
synchronized void l a s t () {
System.out.pri n t l n (внутри A .last");
}
}
class В {
synchronized void bar(A a) {
String name = Thread.currentThread().getName();
System.out.println(name + " вошел в В {
Thread.sle e p (1000);
} catch(Exception e) В прерван + " пытается вызвать A.last()");
a .l a s t ();
}
synchronized void l a s t () внутри A.last");
}
}
class Deadlock implements Runnable {
A a = new A () В b = new В ();

2 8 2 Часть I. Язык Java
Deadlock() {
Thread.currentThread().setName("MainThread");
Thread t = new Thread(this, "RacingThread");
t .sta r t ();
a.foo(b); // получить блокировку внутри этого потока n t l n (Назад в главный поток void run() {
b.bar(a); // получить блокировку b в другом потоке.
System.out.println("Назад в другой поток static void main(String a r g s []) {
new Когда вы запустите эту программу, то увидите следующий результат вошел в A.foo
RacingThread вошел в В пытается вызвать B.last()
RacingThread пытается вызвать A . l a s t Поскольку эта программа заблокирована, вам придется нажать для завершения программы. Вы можете видеть весь потоки дамп кеша монитора, нажав . Вы увидите, что
RacingThread владеет монитором на Ь, в то время как последний ожидает монитора на а. В тоже время
MainThread владеет аи ожидает Ь. Эта программа никогда не завершится. Как иллюстрирует этот пример, если ваша многопоточная программа неожиданно зависла, то первое, что следует проверить, — возможность взаимной блокировки.
Приостановка, возобновление и останов потоков
Иногда возникает необходимость в приостановке выполнения потоков. Например, отдельный поток может использоваться для отображения времени дня. Если пользователю ненужны часы, то этот поток можно приостановить. В любом случае приостановка потока — простая вещь. Выполнение приостановленного потока может быть легко возобновлено.
Механизм временной либо окончательной остановки потока, а также его возобновления отличался в ранних версиях Java, таких как Java 1.0, от современных версий, начиная с Java 2. Хотя при написании нового кода нужно придерживаться нового подхода, вы по-прежнему должны понимать, как эти операции были реализованы в ранних версиях среды Java. Например, может возникнуть необходимость в поддержке или обновлении старого, унаследованного, кода. Вам также может понадобиться понять, почему в этот механизм были внесены изменения. По этим причинам в следующем разделе описан изначальный способ управления выполнением потоков, аза ним следует раздел, описывающий, как это реализовано в новых версиях
Глава 11. Многопоточное программирование 8 Приостановка, возобновление и останов потоков в Java 1.1 и более ранних версиях
До версии Java 2 программы использовали методы s u s p e n d () и resu m e ( ), определенные в классе T h r e a d для приостановки и возобновления потоков. Они имеют следующую форму void suspend()
final void r e Хотя использовать эти методы больше не рекомендуется, в следующей программе демонстрируется их применение, чтобы вы могли понять, как они работали Использование методов suspend() и resu m e () только в демонстрационных
// целях. Для нового кода они не рекомендуются
class NewThread implements Runnable {
String name; // имя потока
Thread t;
NewThread(String threadname) {
name = threadname;
t = new Thread(this, Новый поток " + t ) ;
t.start(); // запуск потока Точка входа потока
public void r u n () {
try {
for(int i = 15; i > 0; i--) {
System.out.println(name + ": " + i);
Thread.sleep(2 00);
}
} catch (InterruptedException e) {
System.out.println(name + " прерван + " завершен SuspendResume {
public static void main(String a r g s []) {
NewThread obi = new Один ob2 = new Два {
Thread.sleep(1000);
o b i .t .suspend();
System.out.p r Приостановка потока Один
o b i .t Возобновление потока Один
o b 2 .t .suspend();
System.out.p r i Приостановка потока Два
o b 2 .t Возобновление потока Два catch (InterruptedException e) Главный поток прерван

2 8 Часть I. Язык Java
// Ожидание завершения потоков
try Ожидание завершения потоков
o b i .t .j o i n ();
o b 2 .t .j o i n ();
} catch (InterruptedException e) Главный поток прерван");
}
System.oat.println("Главный поток завершен.");
}
}
Пример вывода этой программы показан ниже (в вашем случае он может отличаться, в зависимости от скорости и загрузки процессора).
Новый поток T h Один Один Новый поток T h read[Два,5,main]
Два: 15 Один 14
Два 14
Один 13 Два 13 Один Два 12 Один Два Приостановка потока Один
Два: Два Два Два Два б
Возобновление потока Один
Приостановка потока Два
Од и н :
Один Один Один Один б
Возобновление потока Two Ожидание завершения потоков.
Два: 5 Один Два 4 Один Два 3 Один Два Один Два 1 Один Два завершен.
Один завершен.
Главный поток завершен.
Класс
T h r e a d также определяет метод
s t o p (), который останавливает поток. Его сигнатура такова void s t o p (Остановленный поток уже не может быть возобновлен с помощью метода
r e ­
s u m e ().
Глава 11. Многопоточное программирование
2 8 Современный способ приостановки возобновления и остановки потоков
Хотя применение методов suspend(), resume
() и stop
() класса
Thread выглядит как исключительно разумный и удобный подход к управлению выполнением потоков, они не должны использоваться в новых программах Java. И вот почему. Метод suspend
() класса
Thread несколько лет назад был объявлен нежелательным в Java 2. Это было сделано потому, что иногда он способен порождать серьезные системные сбои. Предположим, что поток пытается получить блокировки на критичных структурах данных. Если поток приостановить в этот момент, блокировки не будут установлены. Другие потоки, которые ожидают эти ресурсы, могут оказаться взаимно блокированными.
Метод resume
() также нежелателен. Он не вызовет проблемно не может быть использован без метода suspend
() как своего дополнения.
Метод st ор () класса
Thread также объявлен устаревшим в Java 2. Это было сделано потому, что он также иногда может послужить причиной серьезных системных сбоев. Предположим, что поток выполняет запись в критически важную структуру данных и успел выполнить только частичное обновление. Если его остановить в этот момент, структура данных может оказаться в поврежденном состоянии.
Поскольку вы не можете использовать методы suspend(), resume
() или stop
() для управления потоками, то можете подумать, что теперь вообще нет способа приостановить, возобновить или прервать поток. К счастью, это не так. Вместо этого поток должен быть спроектирован так, чтобы метод run
() периодически проверял, должно ли выполнение потока быть приостановлено, возобновлено или прервано. Обычно для этого используется переменная-флаг, указывающая состояние потока. До тех пор, пока этот флаг имеет значение запущен, метод run
() должен продолжать выполнение. Если флаг имеет значение прерван, поток должен приостановиться. Если флаг получает значение стоп, то поток должен завершиться. Конечно, существует множество способов написать такой код, но основной принцип остается неизменным для всех программ.
В следующем примере показано, как методы wait (
) и notify ()
, унаследованные от класса
Object, могут применяться для управления выполнением потока. Этот пример похож на программу из предыдущего раздела. Однако вызовы устаревших методов здесь исключены. Рассмотрим работу этой программы.
Класс
NewThread содержит переменную экземпляра типа boolean по имени suspendFlag, используемую для управления выполнением потока. Конструктор инициализирует ее значением false. Метод run
() содержит блок synchronized, который проверяет состояние переменной suspendFlag. Если ее значение равно true, вызывается метод wait
() для приостановки выполнения потока. Метод ту) устанавливает значение переменной suspendFlag в состояние true. Метод myresime() устанавливает значение переменной suspendFlag в состояние false и вызывает метод notify
(), чтобы разбудить поток. И наконец, метод main
() модифицирован для вызова методов my suspend
() и myresime
( )
// Приостановка и возобновление потока современным способом
class NewThread implements Runnable {
String name; // имя потока
Thread t;
boolean suspendFlag;
NewThread(String threadname) {
name = threadname;
t = new Thread(this, name);

2 8 Часть I. Язык Java
System.out.p r Новый поток " + t) ;
suspendFlag = false;
t.start(); // запустить поток Точка входа потока
public void r u n () {
try {
for(int i = 15; i > 0; i--) {
System.out.println(name + ": " + i);
Thread.sleep(200);
synchronized(this)
{
w h i l e (suspendFlag) {
w a i t ();
}
}
}
} catch (InterruptedException e) паше + " прерван + " завершен void mysuspend() {
suspendFlag = true;
}
synchronized void myresume() {
suspendFlag = false;
no t i f y ();
}
}
class SuspendResume {
public static void main(String a r g s []) {
NewThread obi = new Один ob2 = new Два {
Thread.sleep(1000);
o b i Приостановка потока Один
Thread.sleep(1000); Возобновление потока Один
ob2.mysuspend();
System, out .println (Приостановка потока Два
Thread.sleep(1000); Возобновление потока Два
} catch (InterruptedException e) Главный поток прерван ожидание завершения потоков
try Ожидание завершения потоков
o b i .t.join();
o b 2 .t .j o i n ();
} catch (InterruptedException e) {
Глава 11. Многопоточное программирование 8 Главный поток прерван");
}
System.out.println("Главный поток завершен");
}
}
Вывод этой программы идентичен приведенному в предыдущем разделе. Чуть позднее в этой книге вы найдете еще примеры, в которых используется современный механизм управления потоками. Хотя этот метод не так чист, как старый, его следует придерживаться, дабы избежать ошибок времени выполнения. Это — подход, который должен применяться во всем новом коде.
Получение состояния потока
Как уже упоминалось в этой главе, поток может находиться в нескольких разных состояниях. Вы можете получить текущее состояние потока, вызвав метод get
St ate ()
, определенный в классе
Thread следующим образом Метод возвращает значение типа
Thread.
State, указывающее состояние потока на момент вызова. Перечисление определено в классе
Thread. Перечисление — это список именованных констант подробно он обсуждается в главе 12.) Значения, которые может возвратить метод getState(), перечислены в табл. Схема на рис. 11.1 демонстрирует взаимоотношения между различными состояниями потока.
С учетом наличия экземпляра класса
Thread, вы можете использовать метод getState ()
, чтобы получить состояние потока. Например, следующий код определяет, находится ли поток по имени thrd в состоянии
RUNNABLE вовремя вызова метода getState ().
Thread.State ts = thrd.getState();
i f (ts == Thread.State.RUNNABLE) // Таблица 11.2. Возвращаемые значения метода
g e t S t a t e ( )
Значения
Состояние
BLOCKED
Поток приостановил выполнение, поскольку ожидает получения блокировки Поток еще не начал выполнение
RUNNABLE
Поток в настоящее время выполняется или начнет выполняться, когда получит доступ к процессору
TERMINATED
Поток закончил выполнение
TIMED_WAITING
Поток приостановил выполнение на определенный промежуток времени, например, после вызова метода s l e e p ( )
. Поток переходит также в это состояние при вызове метода
wait
() или
j oin
()
W Поток приостановил выполнение, поскольку он ожидает некое действие. Например, вызова версий методов
wait
() или
j oin
(), прекращающих ожидание

2 8 Часть I. Язык Ожидание блокировки LO C K Ожидание- К U N N A B L Блокировка получена
i
Ожидание окончено A IT IN G
I или IM E D J W A IT IN G I

T E R M IN A T E Рис. 11.1. Состояния потока
Важно понять, что состояние потока может измениться после вызова метода get
St ate
(). Таким образом, в зависимости от обстоятельств, состояние, полученное при вызове метода getStateO, мгновение спустя может не отражать фактическое состояние потока. По этой и другим причинам метод get
State () не предназначен для использования средствами синхронизации потоков. Он, прежде всего, используется при отладке или для профилирования характеристик потока вовремя выполнения.
Использование многопоточности
Чтобы эффективно использовать многопоточные средства Java вы должны научиться мыслить параллельно, а непоследовательно. Например, когда вы имеете две подсистемы в программе, которые могут выполняться одновременно, оформите их в виде отдельных потоков. При взвешенном применении многопо­
точности выбудете писать очень эффективные программы. Однако следует проявлять осторожность. Если вы создадите слишком много потоков, можете даже снизить производительность всей программы, вместо того чтобы повысить ее. Помните, что переключение контекстов между потоками требует определенных накладных расходов. Если вы создадите очень много потоков, больше времени процессора будет затрачено на переключение контекста, нежели на само выполнение программы Последний момент чтобы создать приложение с интенсивными вычислениями, допускающее автоматическое масштабирование для использования доступных процессоров в многоядерной системе, используйте новую инфраструктуру, описанную в главе 27.

1   ...   16   17   18   19   20   21   22   23   ...   90


написать администратору сайта