У вас вопросы?
У нас ответы:) SamZan.net

можно быть в двух местах одновременно

Работа добавлена на сайт samzan.net: 2016-03-30

Поможем написать учебную работу

Если у вас возникли сложности с курсовой, контрольной, дипломной, рефератом, отчетом по практике, научно-исследовательской и любой другой работой - мы готовы помочь.

Предоплата всего

от 25%

Подписываем

договор

Выберите тип работы:

Скидка 25% при заказе до 11.4.2025

          ГЛАВА 10

       ПОТОКИ вычислений

   

              Как -можно быть в двух -местах одновременно. не будучи нигде?

                           Firesign Theater

Инструкции программ, которые мы обычно пишем, выполняются шаг за шагом в строгой последовательности. Следующий рисунок иллюстрирует ситуацию, когда программа запрашивает значение текущего остатка на банковском счете (getBalance), выполняет приходную операцию, увеличивая остаток на сумму deposit,а затем сохраняет результат в объекте банковского счета (setBalance).

     

     bal = a.getBalance();

     

                                 bal += deposit;

     

      a.setBalance(bal);

Кассир в банке и соответствующая компьютерная программа осуществляют примерно одинаковые действия. Последовательность операций, выполняемых программой по одной в каждый момент времени, принято называть потоком (или питью - thread). Рассмотренная выше модель однотипных вычислений пользуется в среде программистов наибольшей популярностью.

В реальном банке, однако, чаще случается так, что в один и тот же момент времени происходит несколько событий.

 ↓       ↓

 bal = a.getBalance();      bal = b.getBalance();

 ↓       ↓

       bal += deposit;            bal += deposit;

 ↓       ↓

 a.setBalance(bal);         b.setBalance(bal);

      

Программа, отображающая последовательности одновременных действий нескольких кассиров, реализует модель многопоточных вычислений. Поток в программе, как и кассир в банке, способен решать свою собственную задачу независимо от других потоков (кассиров). И поскольку два кассира могут пользоваться

одним и тем же шкафом с картотекой, точно так же два потока в программе вправе обращаться к одним объектам.

Разделение доступа к объектам - это одновременно и наиболее Мощный и плодотворный инструмент многопоточного программирования, и самый серьезный источник недоразумений и ошибок. Схема действий "запросить-изменить-сохранить данные" сопряжена с так называемыми условиями состязаний (race conditions), которые имеют место в тех случаях, когда два потока способны поочередно изменять одну и ту же порцию данных, рискуя нарушить их целостность. Продолжим пример, касающийся изменения состояния банковского счета, и представим, что владелец счета обратился к кассиру с просьбой принять некоторую сумму денег и пополнить остаток. Почти в то же самое время к другому кассиру направился совладелец того же счета и потребовал выполнить собственную приходную операцию.

 ↓       ↓

 bal1 = a.getBalance();      bal 2= b.getBalance();

 ↓       ↓

       bal1 += deposit;           bal2 += deposit;

 ↓       ↓

 a.setBalance(bal1);         b.setBalance(bal2);

Оба кассира проследовали к картотеке, заглянули в одну и ту же папку и получили совершенно одинаковую информацию о текущем остатке на счете (мы говорим, разумеется, о банке, который работает по старинке, используя бумажные технологии). Затем кассиры возвратились к своим стойкам, приняли вклады и вернулись к шкафу с папками, чтобы зафиксировать раздельно выполненные ими операции. Если следовать подобной логике, на состояние остатка повлияет только последняя из двух операций - результат первой будет попросту утрачен.

В банке подобная проблема может быть решена следующим образом: кассир, обратившийся к папке первым, обязан поместить в нее закладку с запиской примерно такого содержания: работаю с этим счетом; подождите, пока не закончу". Практически то же самое делается и в многопоточной программе: объект обозначается признаком блокировки (lock), свидетельствующим о том, что использовать объект запрещено.

Многие реальные задачи могут быть наилучшим образом решены именно в рамках много поточной модели. Например, интерактивные графические программы часто нуждаются в том, чтобы пользователь имел возможность вмешиваться в процесс вычислений в любой момент. Наиболее эффективное средство достижения такой цели связано с использованием нескольких потоков. Напротив, однопоточные системы способны лишь к созданию иллюзии одновременных вычислений; для этого в них используются различные механизмы прерывания по вызову (polling), которые предоставляют поочередный доступ к ресурсам компьютера нескольким частям приложения. В частности, та область кода программы, которая ответственна за графический вывод, должна быть написана таким образом, чтобы ее можно было прерывать настолько часто, насколько это необходимо для обеспечения быстрой реакции системы на действия пользователя.

244        ГЛАВА 10. ПОТОКИ ВЫЧИСЛЕНИЙ

Код, отображающий графику, либо обязан гарантировать, что все его операции требуют минимально возможного процессорного времени, либо должен предусматривать средства "дробления" собственных инструкций на части оптимальной продолжительности. Получаемая в результате смесь двух совершенно различных по своему назначению функций приводит к тому, что программный код становится чрезвычайно сложным и зачастую не поддается дальнейшему развитию.

Подобные проблемы гораздо легче решаются в многопоточной среде - один поток, например, управляет процессами обновления данных на экране, а другой взаимодействует с пользователем. Если информация от пользователя поступает в сложной форме и требует значительного времени на подготовку, поток, контролирующий отображение данных, может выполняться независимо до момента поступления новой порции информации. В системе, реализующей прерывания по вызову, должен быть предусмотрен один из следующих вариантов действий: процесс обновления содержимого экрана либо приостанавливается на время работы процедуры ввода пользователем сложных данных, либо вступает в изощренные "переговоры" с такой процедурой и постепенно выI1лняетсяя поочередно с ней. Все эти вещи в много поточной системе поддерживаются непосредственно, поэтому их не нужно продумывать и реализовывать заново в каждом конкретном случае.

В этой главе описаны языковые конструкции, классы и методы, позволяющие управлять потоками вычислений в Jаvа-программе. Впрочем, это не значит; что нашего материала совершенно достаточно для приобретения знаний и навыков создания эффективных многопоточных программных приложений. За дополнительной, более полной информацией по подобным вопросам вы можете обратиться, например, к другой из книг этой серии - Concurrent Programming in Java™. Заключительный раздел, Список литературы, нашей книги содержит ссылки на другие полезные информационные источники, имеющие отношение к модели многопоточности и синхронизации в Java.

10.1. Создание потоков

Чтобы создать поток вычислений в Jаvа-программе, следует начать с конструирования объекта класса Thread:

Thread worker = new Thread();

После того как объект Thread создан, его можно настроить и запустить на выполнение. Настройка потока включает в себя задание исходного приоритета выполнения, имени и Т.д. Когда объект готов к работе, вызывается его метод start. Метод start, используя данные объекта Thread, порождает новый поток Вычислений и завершает свое выполнение. Далее виртуальная машина Java вызывает метод run объекта и делает поток активным. Вызов start для каждого потока может быть осуществлен только один раз - повторное обращение приводит к выбрасыванию исключения типа i11ega1ThreadStateException.

Когда метод run возвращает управление, выполнение потока завершается. Прервать работу потока можно с помощью вызова метода interrupt - хорошо спроектированный поток всегда на него реагирует. Во время выполнения потока вы можете Взаимодействовать с ним и другими способами, о которых мы расскажем ниже.

Стандартная реализация метода Thread . run не предполагает выполнения каких бы то ни было действий. Чтобы заставить поток делать что-нибудь полезное, необходимо либо расширить класс Thread и предложить собственную пере-

10.1. СОЗДАНИЕ потоков           245

определенную версию метода гип, либо создать объект класса, производного от Runnable, и передать его конструктору потока в качестве аргумента. Сначала мы рассмотрим первый подход, связанный с расширением класса Thread. Вопросам использования объектов Runnablе посвящен следующий раздел.

Ниже приведен пример простой программы, предусматривающей создание двух потоков, которые выводят на экран слова "ping" и "PONG" с различной частотой.

public class РingPong extends Thread {

 private String word;  // Слово, подлежащее выводу на экран

private int delay;  // величина временной задержки

public РingPong(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){

new PingPong(“ping”,33).start();

new PingPong(“ping”,100).start();

Мы определили новый класс РingPong, Производный от Thread. Метод гип содержит бесконечный цикл, в котором выполняются инструкции вывода слова word на экран и приостановки действия потока на период времени, заданный значением delay. Метод РingPong. гип не способен генерировать исключения, поскольку Никакие исключения не указаны в объявлении унаследованного метода Thread. тип. Тем не менее мы должны предусмотреть средства обработки исключения типа InterruptedException, которое может быть выброшено методом slеер (об исключении InterruptedException речь пойдет позже).

Теперь ничего не, мешает создать несколько потоков, и метод main- как раз то место, где можно это сделать. Мы создаем два объекта РingPong, передавая конструктору класса различные аргументы, представляющие слово и интервал задержки, и вызываем для каждого объекта метод start. Потоки активизируются и начинают выполняться. Вот как могут выглядеть результаты их работы:

ping     PONG  ping      ping     PONG  ping      ping      ping      PONG  ping

ping     PONG  ping      ping     ping      PONG  ping      ping      PONG  ping

pong     ping     PONG  ping      ping     PONG   ping      ping      ping      PONG

plng      ping     PONG  ping      ping     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

246         ГЛАВА 10. ПОТОКИ ВЫЧИСЛЕНИЙ

Потоку разрешено присвоить имя - либо с помощью аргумента String, передаваемого конструктору, либо посредством вызова метода setName. Текущее значение имени потока легко получить с помощью метода getName. Имена потоков служат только для удобства программиста (исполняющей системой они не используются) но поток должен обладать именем, и если оно не задано, исполняющая система принимает эту обязанность на себя, генерируя имена в соответствии с некоторым простым правилом, например thread_1, thread_2 и Т.д.

ссылку на объект Thread текущего выполняемого потока можно получить с помощью статического метода Thread. currentThread. Во время работы программы текущий. поток существует всегда, даже если вы не создавали Потоки явно – метод main активизируется с помощью потока, создаваемого исполняющей системой.

Упражнение 0.1. Напишите про грамму, которая отображает имя потока, выполняющего код метода main.

10.2. Использование объектов Runnablе

Поток служит абстракцией понятия исполнителя - субъекта, способного к выполнению каких-либо полезных действий. План работы, подлежащей выполнению, описывается посредством инструкций метода гип. Чтобы некая цель была достигнута, необходимы исполнитель и план работы: интерфейс Runnablе абстрагирует понятие работы и позволяет назначить ее исполнителю - объекту потока. В составе интерфейса Runnablе объявлен единственный метод:

public void run();

Класс Thread реализует интерфейс Runnablе, поскольку поток сам по себе способен определять план работы, подлежащей выполнению.

Вы уже видели, как класс Thread может быть расширен с целью описания Конкретных действий, которые должны выполняться потоком, но такой подход во многих случаях неуклюж и неэффективен. Во-первых, допускается наследовать только один класс - если вы расширяете класс Thread, чтобы обеспечить возможность работы производного класса в виде потока, вы не сможете, даже если очень захотите, унаследовать при этом какой бы то ни было другой класс. Во-вторых, если производный класс нуждается Только в реализации возможности Выполнения, наследование всех "внутренностей" класса Thread чересчур накладно и просто излишне.

Реализация интерфейса Runnable во многих случаях представляется более Простым решением. Код объекта Runnablе может быть выполнен в его собственном Потоке при передаче объекта конструктору Thread. Если объект Thread конструируется на основе объекта Runnablе, из тела реализованного метода Thread. гип вызывается метод гип объекта Runnablе.

Ниже приведен текст версии класса РingPong, реализующей интерфейс Runnablе. Сопоставив оба варианта, вы убедитесь, что они почти совпадают. Основные различия касаются заголовка объявления (теперь класс RunРingPong реализует интерфейс Runnablе, а не расширяет класс Thread) и содержимого Метода main.

10.2. ИСПОЛЬЗОВАНИЕ ОБЪЕКТОВ RUNNAВLE         247

public class RunРingPong implements Runnable {

 private String word;  // Слово, подлежащее выводу на экран

 private int delay;  // Величина временной задержки

RunРingPong(String whatTosay, int delayTime) {

word = whatToSay;

delay = delayTime;

}

public void run() {

 try {

for (;;) {

system.out.print(word + " ");

Thread.sleep(delay); // Приостановить выполнение

}

} catch (InterruptedException е) {

return; // Завершить поток 

}

}

public static void main(String[] args) {

Runnable ping = new RunРingPong("ping",33);

Runnable pong = new RunРingPong("PONG", 100);

new Thread(ping).start();

new Thread(pong).start();

}

}

пределен новый класс, реализующий интерфейс Runnablе. Реализация метода совпадает с прежней, предусмотренной в классе РingPong. В теле метода mаin создаются два объекта RunРingPong с разными уровнями "активности"; для каждого из них конструируется объект потока и немедленно запускаться на выполнение.

Существует четыре перегруженные версии конструктора класса Thread, позволяющие передать в качестве параметра объект Runnablе.

public Thread(Runnable target)

Создает новый объект Thread, использующий метод гип объекта target. public Thread(Runnable target, String name)

Создает новый объект Thread с заданным именем name, использующий метод гип объекта target.

public Thread(ThreadGroup group, Runnable target)

Создает новый объект Thread в указанном объекте ThreadGroup, использующий метод гип объекта target. (За сведениями о классе ThreadGroup обращайтесь к разделу 10.11 на странице 277.)

public Thread(ThreadGroup group, Runnable target, String name) Создает новый объект Thread с заданным именем name в указанном объекте ThreadGroup, использующий метод гип объекта target.

Классы, обладающие только методом гип, вызывают не очень большой интерес. В классе реального приложения должны быть определены полные наборы признаков состояния и характеристик поведения, и подразумевается, что операции, выполняемые в отдельном потоке, - это только часть общих функций.

248         ГЛАВА 10. ПОТОКИ ВЫЧИСЛЕНИЙ

Рассмотрим, например, сервер печати, который получает задания и посылает их на принтер. Клиенты сервера, желающие выполнить печать, вызывают метод print. Функция метода заключается только в том, чтобы помещать задания в очередь; затем отдельный поток извлекает их из очереди и посылает на принтер. Это позволяет клиентам отправлять задания на печать, не дожидаясь завершения выполнения других заданий.

class PrintServer implements Runnable {

private Queue requests = new Queue();

public PrintServer() {

new Thread(this).start(); }

public void print(PrintJob job) {

requests.add(job);

}

public void run() {

for(; ;)

realPrint((PrintJob)requests.take()); }

private void realPrint(PrintJob job) {

// Выполнить фактическую функцию печати

 }

} Конструктор объекта PrintServer создает новый объект Thread, ответственный за выполнение печати заданий, и передает последнему ссылку на себя как экземпляр Runnablе. Старт потока в конструкторе, выполняемый подобным образом, в общем случае оказывается довольно рискованной операцией, если класс впоследствии будет расширяться, - поток таким образом сможет обращаться к полям объекта еще до момента завершения выполнения конструктора производного класса.

Объект очереди requests "заботится" о синхронизации работы различных потоков (мы расскажем о синхронизации ниже и приведем определение класса Queue в разделе 10.4 на странице 259).

Вам может показаться странным тот факт, что после создания объекта потока мы не используем ссылки на него. Не означает ли это, что объект сразу после построения поступает в распоряжение процесса сборки мусора? Нет, такое предположение не соответствует действительности. Хотя мы явно не сохраняем ссылки на поток, он самостоятельно делает это с помощью объекта ThreadGroup (за информацией о группах потоков обращайтесь к разделу 10.11 на странице 277).

План работы, определяемый в теле метода гип, обычно отличается достаточно частным характером и предназначается конкретному исполнителю. Однако run как часть интерфейса обладает признаком public и может быть вызван любым кодом, обладающим достаточными правами доступа к объекту, - это наверняка не то, что предусматривается при нормальных обстоятельствах. Например, мы определенно не хотим, чтобы клиенты обращались к методу run класса PrintServer. Одно из решений состоит в использовании статического метода Thread.currentThread для распознавания "личности" потока, вызвавшего run, и сопоставления его с тем, которому подобное обращение позволено. Но существует и более простой подход - не реализовывать интерфейс Runnablе, а объя-

10.2. ИСПОЛЬЗОВАНИЕ ОБЪЕКТОВ RUNNAВLE         249

Внутренний объект типа Runnablе. Следуя сказанному, можно переписать класса PrintServer следующим образом:

class PrintServer2 {

pr;vate Queue requests = new Queue();

public PrintServer2() {

Runnable service = new Runnable() {

public void run() {

for(; ;)

realPrint((PrintJob)requests.take());

}

};

new Thread(service).start();

}

public void print(PrintJob job) {

requests.add(job);

}

private void  realprint(PrintJob job) {

// выполнить фактическую функцию печати

}

Упражнение 10.2. Исправьте первую версию класса PrintServer таким образом, чтобы возможность выполнения run предоставлялась только тому потоку, который создается в конструкторе класса. Используйте упомянутый выше способ идентификации потока, связанный с использованием Thread. currentThread.

10.3. Синхронизация

Давайте вспомним рассмотренный в начале главы пример, касающийся обслуживания банковских счетов. Когда двум кассирам (программным потокам) требуется обратиться к одной и той же папке (объекту), существует потенциальная возможность порчи и потери данных. Подобные последовательности взаимно опасных действий принято называть критическими секциями (critical sections),

критическими областями (critical regions). Решение проблемы состоит в синхронизации (synchronization) доступа к критическим секциям. Кассир банка синхронизирует свои действия, помещая в папку закладку, и сам в свою очередь, подчиняется общепринятому соглашению, гласящему, что при наличии закладки использование папки запрещено. В многопоточной программе равнозначный

250        ГЛАВА 10. ПОТОКИ ВЫЧИСЛЕНИЙ

эффект достигается за счет применения механизма блокировки (lock) объекта. Потоки взаимодействуют в соответствии с протоколом, согласно которому любому действию над объектом должно предшествовать приобретение (захват) единоличного права на блокировку объекта. Обладание этим правом предотвращает

4

возможность использования объекта другими потоками до тех пор, пока право

Не будет возвращено потоком-владельцем. Если все сделано правильно, потоки не смогут выполнять единовременные действия, чреватые вмешательством в частные "дела" друг друга.

Каждому объекту ставится в соответствие свойство блокировки, и право доступа к нему может быть приобретено и возвращено посредством методов и инструкций, обозначенных признаком synchronized. Термином синхронизированный код (synchronized code) обозначается любой фрагмент кода, который расположен внутри тела метода или инструкции, помеченных как sуnсhronized.

10.3.1. Методы synchronized

Классы, объекты которых должны быть защищены от последствий одновременного доступа со стороны различных частей многопоточного приложения, обычно обладают соответствующими методами, обозначенными модификатором synchronized (о том, какие методы следует считать "соответствующими", мы расскажем ниже). Если один из потоков вызывает synchronized -метод объекта, сначала приобретается право блокировки объекта, выполняется код тела метода, а затем блокировка снимается. Другой поток, вызывающий метод synchronized того же объекта, вынужден приостановить свое выполнение до тех пор, пока блокированный объект не будет освобожден.

Механизм синхронизации вынуждает потоки быть взаимно "вежливыми" и по очереди уступать друг другу право выполнения. При отсутствии синхронизации потоки не ожидают возможности захвата блокировки и выполняются независимо от того, установлен признак блокировки объекта или нет.

Право владения блокировкой предоставляется потоку, поэтому вызов метода synchronized из другого метода synchronized того же объекта выполняется без задержек, а освобождение блокировки выполняется только после завершения работы внешнего синхронизированного метода. Такие правила "игры" дают возможность потоку, владеющему блокировкой, выполняться без остановки и вы-

10.3. СИНХРОНИЗАЦИЯ           251

зывать рекурсивно тот же метод или унаследованные методы, которые также могут быть помечены признаком synchronized.

Блокировка освобождается сразу после того, как прекращается выполнение синхронизированного метода, - либо естественным образом, с помощью инструкции return или по достижении конца кода, либо в результате Возникновение исключительной ситуации. В отличие от систем, где необходимо Принимать меры для захвата и освобождения блокировок, схема синхронизации, принятая в Java, не позволяет "забыть" снять ранее установленную блокировку. Механизм синхронизации управляет выполнением кода, представленного выше: один поток, вызывая синхронизированный метод, получает право исключительного владения объектом и заставляет другой поток, желающий обратиться к тому же объекту, ожидать момента освобождения блокировки.

Класс BankAccount (банковский счет), предназначенный для выполнения в многопоточной среде, мог бы выглядеть следующим образом:

Сass BankAccount {

private long number;

private long balance;

public BankAccount(long initialDeposit) {

balance = initialDeposit;

}

public synchronized long getBalance() {

return balance;

}

public synchronized void deposit(long amount) {

balance += amount;

}

// ... другие методы

Сейчас мы готовы объяснить, что означает термин "соответствующий", употребленный нами в начале раздела, применительно к синхронизированным методам. конструктор не должен обозначаться как синхронизированный.

Поле balance защищено от асинхронных изменений, поскольку доступ к неограничен методами, обозначенными признаком synchronized. Если значение поля изменяется, оно не может быть считано другим потоком до тех пор, а первый не завершит операцию сохранения. Если бы один поток мог читать даннные во время записи их другим потоком, целостность и достоверность считанной информации гарантировать было бы нельзя. Схема синхронизированных действий "запросить-изменить-сохранить данные" предполагает, что значение

Должно изменяться в промежутках между моментами его записи и считывания в противном случае оно считается неверным. Доступ к полям должен быть только синхронизированным. В этом заключается еще одна причина, порой методы доступа (accessor methods) следует предпочесть возможности посредственного

обращения к полям public или protected: используя методы мы можем cсинхронизировать процессы обращения к данным, отчего никоим

252         ГЛАВА 10. потоки ВЫЧИСЛЕНИЙ

образом нельзя достичь, если поля допускают прямое обращение из кода, расположенного за пределами класса.

Используя объявления synchronized, мы можем поручиться, что несколько выполняющихся потоков не будут "пересекаться" друг с другом. Каждый из методов выполняется на правах взаимоисключающего владения объектом - как только стартует один из методов, все другие вызовы любых методов объекта откладываются и могут быть реализованы только после того, как первый завершит работу. Однако система не может предоставить гарантии строгого порядка выполнения операций. Если запрос на чтение содержимого поля balance выдается приблизительно в тот же момент времени, что и вызов метода изменения значения поля, один из них определенно выполнится раньше другого, но мы не можем точно предсказать, какой именно. Если необходимо обеспечить определенный порядок выполнения операций, потоки обязаны координировать свои действия каким-то особым образом, зависящим от назначения и характеристик конкретного приложения.

Когда метод synchronized переопределяется в производном классе, он может либо наследовать признак синхронизации, либо нет. При вызове одноименного метода, принадлежащего базовому классу, тот все еще будет вести себя как синхронизированный. Если метод, не обозначенный как synchronized, обращается к синхронизированному методу базового класса посредством ссылки super, он овладевает блокировкой объекта, которая освобождается после завершения метода базового класса. Атрибут синхронизации - это часть реализации класса. В расширенном классе допускается изменение структуры данных в направлении устранения опасности одновременного доступа к ним, поэтому использовать синхронизированные методы объективно больше не требуется; и наоборот, производный класс вправе расширить набор функций метода, открывая возможности взаимного влияния потоков, обращающихся к методу, и в таком случае последний должен быть обозначен как synchronized.

10.3.2. Статические методы synchronized

Модификатор synchronized допускается применять и в объявлениях статических методов. Каждому классу ставится в соответствие объект типа Class (обратитесь к разделу 11.2.1 на странице 298). Статические синхронизированные методы класса овладевают блокировкой соответствующего объекта Class. Два потока не могут одновременно вызывать статические synchronized методы одного класса - то же запрещено, как было сказано выше, и в отношении нестатических синхронизированных методов конкретного объекта класса. Если к статическим данным могут обращаться различные потоки, доступ к данным должен предоставляться только через посредничество соответствующих статических synchronized -методов.

253          10.3. СИНХРОНИЗАЦИЯ

 10.3.3. Инструкции synchronized

Инструкции synchronized позволяют выполнять синхронизированный код, который способен блокировать произвольный объект, а не только текущий, либо уменьшить длительность блокировки, распространяя ее влияние только на часть метода. Инструкция synchronized состоит из двух частей - ссылки на объект, блокировка которого запрашивается, и фрагмента кода, выполняемого после З:1хвата блокировки. Общая синтаксическая форма synchronized -инструкции вы-

глядит так:

synchronized (выражение) {

Инструкции

}

выражение в результате вычисления должно давать объектную ссылку. После получения права блокировки выполняется блок кода Инструкции, по завершении которого блокировка освобождается, - блокировка снимается даже в том случае, когда внутри фрагмента кода Инструкции выбрасывается исключение, не подвергающееся обработке. Объявление synchronized -метода, рассмотренное выше, - это, на самом деле, сокращенная синтаксическая конструкция для описания метода, тело которого заключено внутрь synchronized -инструкции, использующей для указания на блокируемый объект ссылку this.

Рассмотрим метод, предусматривающий замену значения каждого элемента массива абсолютной величиной и основанный на использовании инструкции synchronized, которая регулирует доступ к массиву:

/** Присвоить элементам целочисленного массива абсолютные величины текущих значений */

public static void abs(int[] values) {

synchronized (values) {

for (int i = о; i < values.length; i++)

{

if (values[i] < 0)

values[i] = -values[i];

}

}

}

Массив values содержит значения, подлежащие изменению. Мы синхронизируем доступ к массиву, указывая идентификатор values в заголовке инструкции synchronized. Теперь можно гарантировать, что во время выполнения внутреннего цикла никакой иной фрагмент кода, помеченный признаком synchronized, не сможет изменить содержимое элементов массива values. ЭТО пример того, что обычно называют синхронизацией на стороне клиента (clientside synchronization), - все клиенты, совместно использующие объект (в нашем случае, массив), обязуются синхронизировать свои действия по отношению к объекту прежде, чем предпринимать попытки манипуляций им. Других способов защиты совместно используемых объектов, подобных массивам, от опасности единовременного доступа не существует, поскольку они не обладают соответствующими методами, которые можно было бы обозначить модификатором synchronized.

Инструкции synchronized обладают целым рядом специальных областей применения и преимуществ по сравнению с synchronized -методами. во-

254        ГЛАВА 10. потоки ВЫЧИСЛЕНИЙ

первых они дают возможность определения синхронизированного участка кода, охватывающего только некоторый фрагмент тела метода. Не надо забывать, что синхронизация оказывает влияние на производительность кода - пока один поток владеет блокировкой, остальные вынуждены ожидать своей очереди, - и поэтому общее правило многопоточного программирования гласит, что блокировка должна охватывать настолько короткий фрагмент кода (и период времени его выполнения), насколько это возможно. Используя инструкцию synchronized, мы можем обращаться к средствам блокирования объекта только тогда, когда это совершенно необходимо. Например, метод, Выполняющий сложные вычисления с последующим присваиванием результатов полям объекта, зачастую нуждается в синхронизации только той части кода, которая непосредственно ответственна за операции присваивания, а не за весь длительный процесс вычислений в целом. Во-вторых, synchronized -инструкции позволяют синхронизировать объекты, отличные от thi5, и дают возможность создавать самые разнообразные схемы синхронизации. Одна из достаточно часто встречающихся ситуаций связана с необходимостью обеспечения более высокого уровня интенсивности конкурентного доступа к коду класса за счет уменьшения размеров блокируемых областей кода. Может случиться и так, что различные группы методов класса работают с разными данными того же класса и в то время как внутри группы синхронизация доступа необходима, взаимная связь между группами отсутствует и синхронизация на этом уровне не нужна. Вместо того чтобы обозначать модификатором synchronized все методы класса, мы можем определить отдельные объекты, подлежащие блокированию в каждой группе методов, и снабдить методы соответствующими инструкциями synchronized. Рассмотрим пример:

class SeparateGroups {

private double aVal = 0.0;

private double bVal = 1.1;

protected Object lockA = new Object();

protected Object lockB = new Object();

public double getA() {

synchronized (lockA){

return aVal;

}

}

public vois setA(double val) {

synchronized (lockA){

aVal = val;

}

}

public double getB() {

synchronized (lockB){

return bVal;

}

}

public vois setB(double val) {

synchronized (lockB){

bVal = val;

}

}

10.3. СИНХРОНИЗАЦИЯ          255

}

public void reset() {

synchronized  (lockA) {

synchronized  (lockB) {

aVal = bVal = 0.0;

}

}

}

}

Две ссылочные переменные, указывающие на блокируемые объекты, объявлены как protected, чтобы расширенные классы смогли корректно Синхронизировать свои собственные методы. Обратите внимание и на то, что метод reset, инициализирующий значения jaVal и bVal, прежде запрашивает право на блокирование обоих объектов, lockA и 1ekB.

Еще одна из часто возникающих ситуаций, в которых удобно использовать инструкции synchronized, связана с необходимостью синхронизации Кода внешнего объекта при обращении к нему из внутреннего объекта:

public class Outer {

private int data;

// ...

private class Inner {

void setOuterData() {

synchronized  (outer.this) {

data = 12;

}

}

}

}

Внутренний объект, как и любой другой объект, синхронизируется независимо - запрос на блокировку внутреннего объекта не оказывает влияния на состояние внешнего объекта точно так же, как и обратное действие. Внутренний класс, которому необходимо синхронизировать доступ к коду внешнего объекта, должен выполнить эту операцию явно, и инструкция synchronized - лучшее средство для достижения подобной цели (альтернативой может быть только объявление в составе внешнего класса специального синхронизированного метода, предназначенного для вызова из тела внутреннего класса).

Если необходимо, чтобы synchronized -инструкция пользовал ась той же блокировкой, что и статические синхронизированные методы класса, для задания блокируемого объекта можно использовать литерал типа class для текущего Класса. Такой подход применим и в том случае, если надлежит предотвратить доступ к статическим данным со стороны нестатического кода. Вновь рассмотрим пример класса Body. В его составе есть статическое поле, NextID, предназначенное для хранения очередного свободного номера, готового для присваивания новому объекту Body. К полю обращается конструктор класса Body без аргументов. Если допустить возможность конкурентного создания объектов Body, при обновлении содержимого поля nextID могут возникнуть недоразумения, связанные с одновременным доступом к нему со стороны нескольких потоков.

256        ГЛАВА 10. потоки ВЫЧИСЛЕНИЙ

Чтобы предупредить подобное поведение программы, поместим внутрь конструктора инструкцию synchronized, запрашивающую право на блокировку объекта вody. class:

Body() {

synchronized  (Body.class) {

idNum = nextID++;

}

}

Инструкция synchronized в теле конструктора захватывает блокировку объекта типа class для объекта Body точно так же, как это делает статический synchronized -метод класса. Было бы неверным использовать для синхронизации ссылку this, поскольку при каждом вызове конструктора она указывает на разные объекты, поэтому блокировка текущего (this) объекта не способна предотвратить возможность одновременного доступа к nextID из нескольких потоков. Также неправильно для получения ссылки на объект типа class для текущего экземпляра использовать метод Object. getclass - в расширенном классе, таком как AttributedBody, он возвратит ссылку на объект class для AttributedBody, но не Body, и вновь вместо одной блокировки будет создано несколько и единовременный доступ к данным окажется вполне возможным. Существует простое правило: следует всегда защищать статические данные с помощью блокировки объекта типа class для класса, в котором эти данные объявлены.

Во многих случаях для защиты кода вместо инструкций synchronized удобнее, тем не менее, использовать synchronized -методы. (Например, в классе Body мы могли бы заключить код, который обращается к полю nextID, внутри статического синхронизированного метода getNextID.) Ответить на вопрос, когда применять один подход, а когда - другой, вам помогут только собственные знания и практический опыт.

Наконец, способность synchronized -инструкций запрашивать блокировку произвольных объектов делает возможным выполнение синхронизации на стороне клиента, как мы уже показали на примере статического метода abs, выполняющего замену значений элементов массива соответствующими абсолютными величинами. Такая способность весьма важна не только с точки зрения защиты кода объектов, не обладающих synchronized -методами, но и для синхронизации последовательностей обращений к объекту. Подробно мы расскажем об этом в следующем разделе.

10.3.4. Приемы синхронизации

Выбор Подходящей схемы синхронизации кода зачастую является сложной задачей, которую нельзя охватить в рамках этой книги, не погружаясь в слишком глубокие детали. Мы постараемся вкратце осветить основные правила синхронизации.

Схема синхронизации на стороне клиента предполагает, что все клиенты, обладающие совместными правами доступа к объекту, при обращении к нему должны прежде запросить блокировку объекта, используя инструкции или методы synchronized. Подобное соглашение довольно непрочно, поскольку зиждется только на благих намерениях клиентов работать по правилам. Предпочтительно заставить совместно используемые объекты защищать свой код посредством собственных synchronized -методов (либо соответствующих synchronized -инструкций в теле

10.3. СИНХРОНИЗАЦИЯ           257

методов). В этом случае клиенты просто не смогут использовать объект какими бы ни было несинхронизированными способами. Такой подход иногда называют Синхронизацией на стороне сервера (server-side synchronization), хотя, по большому счету, это просто расширение объектно-ориентированной концепции, достижение большей степени инкапсуляции характеристик поведения кода За счет сокрытия функций синхронизации.

Подчас случается так, что дизайнер, проектирующий класс, просто не принимает в расчет возможность его будущего использования в многопоточной среде поэтому вообще не прибегает к средствам синхронизации кода. Чтобы применить подобный класс в многопоточном приложении, необходимо решить, использовать ли средства синхронизации на стороне клиента, реализуемые с Помощью synchronized -инструкций, либо создать расширенный класс и переопределить соответствующие методы, объявив их синхронизированными, а затем вызывать из них версии методов базового класса посредством ссылки super.

Если вместо класса используется интерфейс, вы вправе предложить его альтернативную реализацию, предусматривающую замену методов интерфейса соответствующими синхронизированными вариантами, при вызове которых далее адресует другой, не обязательно синхронизированный, объект, реализующий тот же интерфейс. Такая схема позволяет работать с любой реализацией интерфейса, поэтому ее можно считать более удачным решением, нежели расширение каждого класса с объявлением синхронизированных переопределенных методов и последующим вызовом исходных методов базового класса с помощью ссылки super. гибкость, присущая интерфейсам, служит еще одним серьезным доводом в пользу

х применения в практике проектирования программного обеспечения. Техника построения подобных синхронизированных реализаций-оболочек находит применение в классах коллекций (обратитесь к разделу 16.8.1 на странице 457).

Синхронизация, о которой мы говорили до сих пор, - это простейший из способов выражения понятия "безопасности потоков". Если подобные последовательности операций должны выполняться как единое целое, без синхронизации просто не обойтись. В случае многократных обращений к методу их можно поместить внутри другого метода и снабдить последний модификатором synchronized, но, вообще говоря, это не очень эффективно, поскольку нам вряд ли удастся определить все комбинации вызовов методов. Более того, на этапе проектирования класса мы можем и не знать, какие сочетания методов потребуются в дальнейшем. При необходимости выполнения операций над несколькими объектами возникает вопрос, куда именно следует поместить синхронизированный метод. В подобных случаях единственной разумной альтернативой будет синхронизация на стороне клиента, реализованная посредством synchronized -инструкций. Объект может блокироваться и запрещать обращение к любому из своих синхронизированных методов всем, кроме кодавладельца блокировки, выполняющего серию вызовов. Аналогичным образом можно запрашивать блокировку каждого отдельного объекта, участвующего в последовательности операций, а затем вызывать требуемые методы этого объек-

258       ГЛАВА 10. потоки ВЫЧИСЛЕНИЙ

та, но при таком подходе следует учитывать опасность возникновения взаимоблокировки (deadlock) (подробная информация представлена в разделе 10.7 на странице 268). Если методы объекта уже синхронизированы относительно блокировки текущего объекта, в других фрагментах кода, использующих объект, нет необходимости применять синхронизацию на стороне клиента.

То, каким образом синхронизация осуществляется в пределах конкретного класса, зависит от особенностей реализации. Тот факт, что класс требует синхронизации доступа к его членам, является важной частью контракта класса и должен быть исчерпывающим образом документирован - наличие модификатора synchronized в объявлении метода может оказаться всего лишь частной деталью реализации класса, а не атрибутом контракта. Кроме того, механизм синхронизации, использованный в коде класса, также может быть документирован, если он представляет интерес для пользователей класса или тех программистов, которые займутся расширением последнего. Расширенному классу следует придерживаться политики синхронизации, оговоренной в контракте базового класса, и он сможет выполнить свою задачу только в том случае, если его автор осведомлен о том, что представляет собой такая политика, и обладает доступом к механизмам, которые ее поддерживают. Например, класс, использующий в качестве блокируемого объекта поле ргivate, воспрепятствует применению в расширенном классе того же механизма блокировки - расширенному классу придется определить свой собственный блокируемый объект (возможно, this) и переопределить каждый метод базового класса, чтобы реализовать новый механизм блокировки. Пользователям класса, вероятно, потребуется знать, какая схема синхронизации реализована, чтобы они могли безопасно применять синхронизацию на стороне клиента при многократном вызове методов объекта.

Упражнение 10.3. Создайте класс, объекты которого способны сохранять некоторое текущее значение в поле value и обладают методом add, позволяющим увеличивать содержимое value и выводить на экран результат операции. Напишите программу , которая создает объект класса, формирует несколько потоков и вызывает внутри каждого из них метод add. Учтите, что ни один из результатов сложения не должен быть утрачен.

Упражнение 10.4. Измените код решения упражнения 10.3 в предположении, что поле value и метод add являются статическими.

Упражнение 10.5. Измените код решения упражнения 10.4 в предположении, что Потоки должны быть способны безопасно изменять содержимое value без использования статического синхронизированного метода.

10.4. wait, notifyAll и notify

Механизмы синхронизированной блокировки позволяют успешно предотвратить возможное взаимное влияние нескольких потоков, но нам нужны, кроме Того, средства обеспечения взаимодействия потоков. С этой целью применяются метод ожидания, wait, позволяющий приостановить выполнение потока до того Момента, пока не будет удовлетворено определенное условие, и методы оnовеще1tuя, notifyAll и notify, которые сообщают ожидающим потокам о том, что произошло некое событие, способное повлиять на результат про верки условия Ожидания. Методы wait, notifyAll и notify определены в составе класса

10.4. WAIT. NOTIFYALL И NOTIFY          259

Object и наследуются всеми производными классами. Они применяются по отношению к конкретным объектам - точно так же, как и блокировки.

Существует стандартный образец кода, которому важно следовать при использовании методов ожидания и оповещения. Потоку, ожидающему выполнения некоторого условия, всегда надлежит выполнять действия, подобные таким:

synchronized void doWhenCondition() {

whilе (! условие)

wait() ;

/ / ... Выполнить то, что необходимо, если условие равно true '"

}

Здесь необходимо сделать ряд замечаний.

Все функции по обеспечению взаимодействия потоков должны выполняться в рамках синхронизированного кода. Если это требование не удовлетворяется, состояние объекта не может считаться стабильным. Например, если метод не объявлен как synchronized, после выполнения блока whilе нельзя твердо гарантировать, что проверяемое условие остается равным true, поскольку другой поток может изменить ситуацию.

Один из важных аспектов контракта метода wait состоит в том, что при приостановке выполнения потока он атомарным образом освобождает блокировку объекта. Говоря об атомарной связи между приостановкой выполнения потока и освобождением блокировки, мы имеем в виду, что эти действия неразделимы и выполняются только совместно. В противном случае существовала бы опасность возникновения условия состязания: событие оповещения могло бы произойти после освобождения блокировки, но перед приостановкой выполнения потока (оповещение не повлияло бы на поток и уведомление оказалось бы попросту утерянным). Когда поток после получения "разрешающего" уведомления возобновляет свою работу, блокировка атомарным образом восстанавливается.

Условие ожидания должно всегда проверяться циклически. Не думайте, что достаточно про верить его только один раз, - после того как условие удовлетворено, оно может измениться вновь. Другими словами, нельзя заменять whilе на if.

Методы оповещения, в свою очередь, вызываются синхронизированным кодом и изменяют одно или несколько условий, разрешения которых могут ожидать какие-либо другие потоки. Код оповещения обычно выглядит следующим образом:

synchronized void changecondition() {

// ... изменить некоторое значение,

// используемое в выражении условия ожидания ...

notifyALL(); // или notify();

}

Метод notifyALL оповещает все ожидающие потоки, а notify выбирает для этого только один поток.

Потоки могут ожидать выполнения условий (возможно, различных), Относящихся к одному и тому же объекту. Если условия действительно различны, для оповещения всех ожидающих потоков следует всегда использовать метод notifyAll вместо notify. В противном случае может случиться так, что уве-

260         ГЛАВА 10. ПОТОКИ ВЫЧИСЛЕНИЙ

домление попадет к какому-нибудь потоку, который, на самом деле, ожидает выполнения другого условия. Этот поток обнаружит, что его условие все еще не разрешено положительно, и вновь перейдет в состояние ожидания, а поток, действительно заинтересованный в информации, которую выдает notify, возможно, так никогда ее и не получит. Метод notify позволяет несколько повысить эффективность кода и может при меняться только в тех случаях, когда:

все потоки ожидают выполнения одного и того же условия;

самое большее один поток приобретет преимущества ввиду выполнения условия;

• подобному контракту подчиняются все возможные производные классы.

ВО всех остальных ситуациях надлежит использовать notifyALL. Если производный класс не удовлетворяет любому из двух условий, указанных в начале списка, работоспособность кода базового класса, в котором применяется notify, может быть нарушена. В этом смысле важно, чтобы информация о принятых классом стратегиях ожидания и оповещения, в том числе и о применяемых объектных ссылках (this или других), была документирована с целью Возможного использования при проектировании расширенных классов.

Следующий пример иллюстрирует реализацию класса Queue (очередь), ссылка на объект которого присутствовала в коде класса PrintServer, рассмотренного в разделе 10.2 на странице 247. В составе класса объявлены методы, позволяющие помещать элементы в очередь и удалять их.

 class Сеll {  // Элемент очереди

Сеll next;  // Ссылка на следующий элемент очереди

Object item;  // Содержимое текущего элемента

cell(Object item) {

this.item = item;

}

}

class Queue {

private Cell head, tail; // Элементы в "голове" и "хвосте" очереди

public synchronized void add(Object о) {

Cell р = new Cell(0); // представить объект о

// в виде элемента очереди

if (tail == null) head = р;

else

tail.next = р;

p.next = null;

tail = р;

notifyALL();   // Оповестить все ожидающие потоки о том,

// что в очередь добавлен элемент

}

public synchronized Object take() throws InterruptedException

{

10.4. WAIT. NOTIFYALL И NOTIFY           261

while (head == null)   // Ждать уведомления о добавлении

 wait() ;    // элемента

 

Cell p = head;   // Запомнить элемент, занимающий место

// в "голове" очереди

head = head.next;

if (head == null)

 tail = null;

return p.item;

}

}

Приведенная здесь реализация очереди схожа с теми, которые применяются в однопоточном программировании, - такими как, скажем, singleLinkQueue (см. раздел 3.5 на странице 102). Отличия касаются нескольких аспектов: методы обозначены модификатором synchronized, предотвращающим опасность взаимного влияния потоков; при добавлении элемента в очередь ожидающие потоки оповещаются; если очередь становится пустой, метод take, вместо того чтобы возвратить null, ожидает, пока какой-либо другой поток добавит в очередь элемент, и поэтому выполнение take блокируется до момента появления в очереди доступного элемента. Многие потоки (не только один) способны добавлять элементы в очередь и извлекать их из очереди. Поскольку метод wait Может генерировать исключение типа interruptedException, последний необходимо упомянуть в предложении throws объявления метода take (подробные сведения об исключении InterruptedException приведены ниже).

Если сейчас вновь возвратиться к примеру класса PrintServer, вы поймете, что хотя, как кажется, основная работа внутреннего потока сосредоточена в бесконечном цикле и сводится к непрерывным попыткам извлечения из очереди очередного задания на печать, обращение к методу take (и далее к wait) означает, что поток приостанавливает выполнение, если в очереди отсутствуют элементы. Если бы мы, напротив, использовали версию класса очереди, в котором метод take в случае, если очередь пуста, возвращал бы null, потоку пришлось бы обращаться к take непрерывно и расходовать при этом ресурс процессора такую ситуацию принято обозначать термином активное ожидание (busywaiting). В многопоточных системах подобный подход используется крайне редко. Поток должен всегда приостанавливать выполнение до тех пор, пока условие, необходимое для продолжения его работы, не будет удовлетворено. Именно в этом состоит суть процессов взаимодействия потоков посредством механизмов wait и notifyALL/notify.

10.5. Механизмы ожидания и оповещения

Существуют три формы методов ожидания и две - оповещения. Все они реализованы в составе класса Object и снабжены модификатором final, препятствующим их переопределению в производных классах.

public final void wait(long timeout) throws InterruptedException Текущий поток ожидает наступления одного из четырех событий: вызван метод notify объекта и потоку следует продолжить работу; вызван метод notifyALL объекта; истек промежуток времени, заданный параметром

262         ГЛАВА 10. ПОТОКИ ВЫЧИСЛЕНИЙ

timeout; поток вызвал собственный метод interrupt. Значение timeout выражается в миллисекундах. Если оно равно нулю, метод будет выполняться неопределенное время до получения уведомления, посланного от notify или notifyALL. На время цикла ожидания блокировка объекта освобождается и автоматически устанавливается непосредственно перед завершением выполнения wait, независимо от того, как и почему ожидание прекращается. Если работа wait завершается в результате вызова метода interrupt, выбрасывается исключение типа InterruptedException.

public final void wait(long timeout, int nanos)

throws InterruptedException

Более чувствительная версия метода. Величина интервала ожидания складывается из двух составляющих: timeout (выраженной в миллисекундах) и nanos (в наносекундах). Величина nanos должна находиться в промежутке 0-999999.

public final void wait() throws InterruptedException Метод аналогичен первому варианту при условии wait (0).

public final void notifyAll()

Оповещает все потоки, ожидающие выполнения условия. Перед выходом из состояния ожидания потоки восстанавливают блокировку объекта.

public final void notify()

Оповещает, самое большее, один из потоков, ожидающих выполнения условия. Выбрать поток, подлежащий оповещению, нельзя, поэтому метод следует применять только в тех случаях, когда вы знаете, какой поток и когда находится в состоянии ожидания. Если хотя бы один из этих факторов не известен, надлежит использовать метод оповещения notifyAll.

Если в момент вызова notifyALL или notify нет потоков, находящихся в состоянии ожидания, уведомление, посланное методами, не запоминается. Если поток переходит в стадию. ожидания периодически, прежние обращения к методам оповещения на него не воздействуют. Результативными оказываются только те уведомления, которые посылаются после вызова потоком метода wait.

Все рассмотренные методы могут вызываться только из синхронизированного кода при использовании блокировки соответствующего объекта. Обращение к методам осуществляется либо непосредственно из синхронизированного кода, либо косвенно, из тела метода, вызванного из такого кода. Если предпринимается попытка использования методов объекта, право на блокировку которого отсутствует, генерируется исключение типа IllegalMonitorstateException.

Если выполнение метода wait завершается ввиду истечения периода ожидания, поток не в состоянии "узнать" об этом каким-либо иным способом, кроме как в результате оповещения. Если потоку необходимо обладать данными о том, Подошло ли к концу время ожидания, он должен сам предусмотреть средства их Получения. Задание ограниченного периода ожидания можно рассматривать как защитную меру, позволяющую справляться с ситуациями, когда некоторое условие должно быть удовлетворено, но по каким-то причинам (вероятно, связанным с нарушением работоспособности другого потока) этого не происходит.

В некоторых реализациях допускаются также так называемые ложные срабатывания (spurious wakeups), когда поток завершает выполнение wait по причинам, не связанным ни с одним из событий, таких как истечение периода ожи-

10.5. МЕХАНИЗМЫ ОЖИДАНИЯ И ОПОВЕЩЕНИЯ       263

даниЯ, прерывание методом interrupt или получение уведомления от метода оповещения. Это еще одна причина, по которой вызов wait следует помещать в тело цикла, проверяющего условие ожидания.

Упражнение 10.6. Напишите программу, которая каждую секунду отображает на экране данные о времени, прошедшем от начала сессии, а другой ее поток выходит сообщение каждые пятнадцать секунд. Предусмотрите возможность ежесекундного оповещения потока, воспроизводящего сообщение, потоком, Отсчитывающим время. Не внося изменений в код потока-"хронометра", добавьте еще один поток, который выводит на экран другое сообщение каждые семь секунд.

1О.6. Упорядочение потоков во времени

Потоки программного приложения решают собственные задачи, различные по степени значимости. В качестве количественного показателя важности Выполняемых функций потоку ставится в соответствие приоритет (priority), значение которого используется системой для определения того, какой из потоков должен выполняться в каждый момент времени. Jаvа-программы - одно- или многопоточные - способны работать как на однопроцессорных, так и на многопроцессорных системах, поэтому о решении задачи управления потоками во времени можно говорить только В самых общих терминах. В системе с N процессорами 'одновременно может выполняться N высокоприоритетных потоков. Потокам, Обладающим более низкими значениями приоритета, ресурсы процессоров обычно отдаются только в том случае, когда более важные потоки блокированы. Чтобы предотвратить вероятность зависания, система вправе предоставлять ресурсы низкоприоритетным потокам и в другие моменты времени - в связи с так называемым старением приоритетов (priority aging), - но прикладные программы {е в состоянии серьезно использовать такую возможность.

Работа потока продолжается до тех пор, пока не будет заблокирована вследствие выполнения wait, sleep или некоторых операций ввода-вывода) либо временно прервана - из-за того, что приступил К работе поток с более высоким приоритетом либо планировщик заданий принял решение о передаче ресурсов другому потоку (например, ввиду истечения времеии (time slice), ) отпущенного на непрерывную работу потока).

Определение расписания приоритетного обслуживания потоков с прерываниями (preemption) входит в компетенцию конкретной виртуальной машины fava. Зачастую твердых гарантий поведения системы в отношении планирования заданий не существует - можно только ожидать, что предпочтение в том или IHOM случае будет отдано потоку, обладающему более высоким приоритетом. атрибуты приоритетов следует при менять только в целях воздействия на системную политику упорядочения потоков для повышения общей производительности программы. Базовый алгоритм приложения никоим образом не должен зависеть от схемы расстановки приоритетов потоков. Намереваясь создать правильный многопоточный код, не зависящий от платформы, мы обязаны принять во внимание, что выполнение каждого потока может быть приостановлено в лю50Й момент. Поэтому следует всегда заботиться о надежной защите ресурсов, находящихся в совместном пользовании. Если же ввиду объективной необходимости следует учитывать возможность приоритетного прерывания работы потоков в определенные моменты времени, нам придется явно применять схемы взаимо-

264         ГЛАВА 10. ПОТОКИ ВЫЧИСЛЕНИЙ

действия потоков, использующие методы ожидания и оповещения. Кроме того не в состоянии выдвигать какие бы то ни было предположения о порядке, в котором права на блокировку объекта раздаются потокам, а также о последовательности получения потоками уведомлений, посланных методами оповещения, - все это является прерогативой исполняющей системы.

исходное значение приоритета потока соответствует приоритету потока. Величина приоритета может быть изменена посредством вызова метода setPriогity с аргументом из интервала, который задается значениями именованных констант MIN_PRIORIТY и MAX-PRIORIТY, определенных в составе класса Thread. Приоритету потока, предлагаемому по умолчанию, соответствует константа NORM_PRIORIТY. Приоритет работающего потока допускается изменять в любой момент. Если приоритет потока понижается, система может передать вычислительные ресурсы другому потоку, поскольку исходный утратит "членство" в группе потоков с наивысшими приоритетами. Чтобы получить текущее значение приоритета потока, следует воспользоваться методом getpriогity.

Вообще говоря, непрерывно выполняющаяся часть приложения должна обладать более низким приоритетом в сравнении с теми потоками, которые обслуживают события, происходящие реже (скажем, связанные с интерфейсом пользователя). Если, например, пользователь щелкает мышью на кнопке Отмена, он предполагает, что приложение должно прекратить текущую работу. Если функции обновления экрана и обработки вводимых пользователем данных обладают одинаковым приоритетом, между событием пользовательского интерфейса и ответной реакцией системы может пройти немало времени. Если присвоить потоку, обновляющему содержимое экрана, более низкий приоритет, он все еще будет выполняться весьма активно, поскольку поток, отвечающий за интерфейс пользователя, блокируется в ожидании действий пользователя. Как только происходит событие, связанное с интерфейсом пользователя, соответствующий поток прерывает работу потока обновления экрана, чтобы обработать введенные пользователем данные. Для потока, занимающегося обновлением экрана, обычно задается приоритет, равный NORM_PRIORITY+l, что позволяет избежать "захвата" потоком всех квантов процессорного времени, а потоку пользовательского интерфейса часто присваивается приоритет NORM_PRIORITY+l.

Обычно более предпочтительно дифференцировать потоки по приоритетам, используя небольшие отклонения относительно уровня NORM_PRIORITY, нежели Впадать в крайности, обращаясь к значениям MIN_PRIORITY и MAX_PRIORITY. Эффект задания тех или иных значений приоритетов зависит от особенностей исполняющей системы, и некоторые системы расценивают приоритет потока как Показатель его значимости не только в рамках конкретного приложения, но и Относительно всех других приложений, выполняемых системой в то же самое Время. Предельные значения приоритетов способны привести к неожиданным последствиям, и если вы не уверены в правильности своего решения, их использования следует избегать.

10.6.1. Принудительное переупорядочение

в составе класса Thread есть несколько методов, которые позволяют потоку уменьшить его потребность в ресурсе процессорного времени. В соответствии с принятым соглашением, статические методы класса Thread всегда применяются по отношению к текущему работающему потоку; поскольку другой поток не

10.6. УПОРЯДОЧЕНИЕ ПОТОКОВ ВО ВРЕМЕНИ        265

может выполнить явный захват ресурсов,' все методы принудительного переупорядочения потоков объявлены как статические.

public static void sleep(long millis) throws InterruptedException Вынуждает текущий работающий поток приостановить выполнение ("уснуть") на период времени, не меньший заданного (время указывается В миллисекундах). Точность промежутка времени приостановки не гарантируется из-за возможного влияния расписания работы других ПОТОКОВ, погрешностей дискретизации системного таймера и иных факторов. Если Во время "спячки" вызывается метод intеrrupt, выбрасывается исключение типа InterruptedException.

public static void sleep(long millis, int nanos)

throws InterruptedException

Вынуждает текущий работающий поток "уснуть" на период времени, не меньший заданного (время указывается в миллисекундах и наносекундах). Величина nanos должна находиться в промежутке 0-999999.

public static void yield(); 

Сообщает планировщику заданий, что текущий поток в данный момент можно не выполнять и вычислительные ресурсы при необходимости следует передать другому потоку. Планировщик вправе последовать этому совету или оставить его без внимания - вообще говоря, нам остается верить в то, что планировщик "поступает правильно", хотя строгих спецификаций этого процесса не существует.

Следующий при мер иллюстрирует возможность воздействия на расписание работы потоков с помощью метода уiеld. Программа принимает в качестве параметров командной строки список слов и создает отдельные потоки, ответственные за вывод на экран каждого из них. Первый параметр указывает, следует ли потоку после очередного вызова println выполнять обращение к методу уiеld второй задает количество повторений каждого слова на экране; все остальные параметры - это слова, подлежащие обработке.

class Babble extends Thread {

static boolean doYield;  // выполнять ли вызов yield?

 static int howoften;  // Количество повторений слова

на экране

private String word;  // Слово, подлежащее обработке

Babble(String whatToSay) { word = whatToSay;

}

public void run() {

for (int i = о; i < howoften; i++) {

System.out.println(word);

if (doYield)

Thread.yield(); // предоставить ресурсы другим потокам

 }

}

266         ГЛАВА 10. ПОТОКИ ВЫЧИСЛЕНИЙ 

public static void main(String[] args) {

doYield = new Boolean(args[0]).booleanValue();

howoften = Integer.parseInt(args[l]);

// создать потоки для обработки каждого слова

for (int i = 2; i < args.length; i++)

new Babble(args[i]).start();

 }

}

Когда вызов уiеld не предусмотрен, каждый поток захватывает продолжительные "куски" процессорного времени, выполняя собственную работу до конца и не позволяя другим потокам "вклиниться" в процесс. Предположим, что программа запущена на выполнение таким образом, чтобы полю doYield было присвоено значение false:

Babble false 2 Быть Не_Быть

Результат работы программы в этом случае, вероятнее всего, будет выглядеть так:

Быть

Быть

Не_Быть

Не_Быть

Если каждый поток после выполнения println обращается к уiеld, возможность работы предоставляется и другим потокам. Предположим, что теперь программа запускается следующим образом:

Babble true 2 Быть Не_Быть

Вызов уiеld объекта одного потока дает шанс "отличиться" другим потокам, а те, в свою очередь, также обращаются к уiеld, и в итоге может получиться нечто подобное:

Быть

Не_Быть

Не_Быть

Быть

Показанный текст можно рассматривать только как пример - вы, вероятно, ожидали, что слова будут чередоваться? Другая реализация потока способна привести к иным результатам, или одна и та же реализация может давать неодинаковые результаты в различных сеансах работы приложения. Но в любой Ситуации применение уiеld означает, что система приостанавливает работу текущего потока и предоставляет ресурсы другим потокам.

Существуют два других фактора, оказывающих влияние на поведение этой программы (и всех других подобных программ, которые можно написать для демонстрации особенностей упорядочения потоков во времени). Первый связан с Тем, что метод println сам по себе использует механизм синхронизации, и всем Потокам приходится оспаривать право на владение одной и той же блокировкой. Теперь о втором факторе _ в нашей программе "живут", на самом деле, три потока,  а не два, как может показаться на первый взгляд. Поток метода main ответствен за создание и запуск на выполнение двух потоков Babble, а это означает, что он также состязается за ресурс процессорного времени. Вполне возможно, 'ITO первый поток Babble успеет завершиться полностью даже раньше, чем по-

10.6. УПОРЯДОЧЕНИЕ ПОТОКОВ ВО ВРЕМЕНИ         267

току таin будет предоставлен малейший шанс создать и запустить на выполнение второй поток Babble.

Упражнение 10.7. Запустите программу Babble несколько раз и про верьте результаты - всегда ли они одинаковы? Если возможно, протестируйте программу в различных системах и сопоставьте итоги.

  1.  Взаимоблокировки

Всякий раз, когда имеются два потока и два объекта, подлежащих блокированию, возникает опасность возникновения взаимоблокировки (deadlock) - каждый из потоков владеет блокировкой одного объекта и ожидает освобождения другого объекта. Если объект Х обладает synchronzеd -методом, который вызывает synchronzеd-метод объекта У, а У, в свою очередь, также имеет синхронизированный метод, обращающийся к synchronzеd -методу объекта Х, Два потока могут находиться в состоянии ожидания взаимного завершения, чтобы овладеть блокировкой, и ни один из них не окажется способен продолжить работу. Такую тупиковую ситуацию на профессиональном жаргоне называют "смертельным объятием", или клинчем (deadly embrace). Ниже приведен пример класса Friends (друзья), в котором каждый из "друзей", обнимая (hug) другого, пытается избавиться (hugBack) от объятий партнера:

class Friends {

private Friends partner; private String name;

public Friends(String name) {

this.name = name;

}

public synchronized void hug() {

System.out.println(Thread.currentThread().getName()+ " в " + name + ". hug () пытается вызвать " + partner. name + ". hugBack() ") ;

partner.hugBack();

}

private synchronized void hugBack() { system.out.println(Thread.currentThread().getName()+ " в " + name + ". hugBack() ") ;

}

public void becomeFriend(Friends partner) {

this.partner = partner;

}

Рассмотрим следующий сценарий, действующими лицами которого являются jareth и сory, два объекта класса Friends, ставшие "друзьями":

  1.  поток 1 вызывает synchronzеd -метод jareth.hug; теперь поток 1 владеет блокировкой объекта jareth;

268         ГЛАВА 10. ПОТОКИ ВЫЧИСЛЕНИЙ

  1.  поток 2 вызывает synchronzеd -метод cory. hug; теперь поток 2 владеет блокировкой объекта cory;

3)  jareth.hug вызывает synchronzеd -метод cory.hugBack; поток 1 приостанавливает выполнение, переходя в                 стадию ожидания возможности захвата блокировки cory (которой в данный момент владеет поток 2);

4) наконец, соrу.hug вызывает synchronzеd -метод jareth. hugBack; поток 2 приостанавливает выполнение, переходя в стадию ожидания возможности захвата блокировки jareth (которой в данный момент владеет поток 1).

Имеет место состояние взаимоблокировки: cory не может продолжить работу, пока не будет освобождена блокировка jareth, и наоборот - и два потока попадают в тупиковую ситуацию.

попытаемся реализовать рассмотренный сценарий в виде следующего кода:

public static void main(String[] args) {

final Friends jareth = new Friends("jareth");

final Friends cory = new Friends("cory");

jareth.becomeFriend(cory);

cory.becomeFriend(jareth);

new Thread(new Runnable() {

public void run() { jareth.hug(); } }, "потокl"). startO;

new Thread(new Runnable() {

public void run() {

cory.hug(); } }; "поток2"). Start() ;

}

После старта, прежде чем "зависнуть", программа успеет вывести на экран примерно следующее:

Поток1 в jareth. hugO пытается вызвать cory. hugBack() Поток2 в cory.hug() пытается вызвать jareth.hugBack()

Разумеется, не исключено, что вам повезет, и один из потоков сумеет выполнить код hug целиком еще до момента старта второго потока. Если бы действия, предусмотренные п. 2) и 3), выполнялись В обратном порядке, объект jareth завершил бы выполнение как hug, так и hugBack до того, как объекту cory потребовалась бы блокировка jareth. Но при очередном выполнении программы опасность попадания в тупик все равно остается, поскольку планировщик заданий вправе осуществить другой выбор. Проблема может быть решена несколькими способами. Простейший из них состоит в том, чтобы удалить из объявлений методов hug и hugBack признак synchronized, но синхронизировать оба метода относительно единого объекта которым совместно владеют все объекты Friends. В таком случае может быть выполнена только одна операция hug, что позволяет предотвратить возможность возникновения взаимоблокировки. Более сложные решения в Состоянии разрешить одновременное выполнение нескольких hug и исключить опасность попадания приложения в тупик.

Полную ответственность за появление и предотвращение подобных ситуаций Несет программист. Исполняющая система не может ни выявить, ни предупредить Опасность взаимоблокировок. Отладка кода, чреватого тупиковыми ситуациями,

10.7. ВЗАИМОБЛОКИРОВКИ          269

нередко весьма сложна, поэтому следует избегать возникновения проблемы еще на стадии проектирования. Один из общих подходов носит название упорядочения ресурсов (resource ordering). Следуя ему, вы должны присвоить порядковые номера всем блокируемым объектам и затем запрашивать блокировку именно в таком порядке. В этом случае исключается возможность возникновения ситуации, Когда два потока владеют блокировками и пытаются захватить блокировки, принадлежащие "сопернику": оба потока должны запрашивать блокировки в строгом порядке - как только первый поток овладеет первой блокировкой, второй, пытающийся получить в свое распоряжение ту же блокировку, будет заблокирован, и тогда первый поток сможет безопасно приобрести вторую блокировку.

Упражнение 10.8. Поэкспериментируйте с программой Friends. Насколько часто взаимоблокировки возникают в вашей системе на самом деле? Если добавить в текст вызовы метода уiеld, поможет ли это избежать тупиковых Ситуаций? Если возможно, попытайтесь проверить работу программы с помощью не. скольких исполняющих систем. Попробуйте устранить возможность появления взаимоблокировок, не затрагивая признаков синхронизации.

10.8. Завершение выполнения потока

О потоке, приступившем к работе, говорят как о действующем (alive), и Метод isAlive такого потока возвращает значение true. Поток продолжает оставаться действующим до тех пор, пока не будет остановлен в результате возникновения одного из трех возможных событий:

метод run завершил выполнение нормальным образом;

работа метода run прервана;

вызван метод destroy объекта потока.

Возврат из метода run посредством return или в результате естественного завершения кода - это нормальный способ окончания выполнения потока. Каждый поток решает определенную задачу, и когда решение получено, работа потока должна быть прекращена. Если, однако, в процессе работы потока возникает какая-нибудь проблема, приводящая к выбрасыванию исключения, которое не подвергается обработке, выполнение потока также завершается (мы обсудим такую ситуацию позже, в разделе 10.12 на странице 281). К моменту прекращения работы потока он освобождает все блокировки, которыми владеет, поскольку при выходе из run выполнение любого синхронизированного кода должно быть завершено.

Вызов метода destroy объекта потока - это совершенно радикальный и непоправимый шаг. В этом случае поток "умирает" внезапно, независимо от того, что именно он выполняет в данный момент, и не освобождает ни одной из захваченных блокировок, поэтому остальные потоки могут остаться в состоянии бесконечного ожидания. Метод destroy относится к разряду последних решительных мер, когда способы взаимно корректного завершения работы потоков (мы расскажем о них ниже) оказываются недейственными. Многими системами, однако, метод destroy не поддерживается, и его вызов влечет выбрасывание исключения типа NoSuchMethodError, способного остановить работу потока-инициатора, а не того потока, завершение которого предусматривалось.

270         ГЛАВА 10. ПОТОКИ ВЫЧИСЛЕНИЙ

Выполнение потока может быть прервано также в том случае, когда завершает работу все приложение целиком (подробнее об этом - в разделе 10.9 на странице 275).

10.8.1. Корректное завершение работы потока

Нередки случаи, когда поток создается для достижения определенной цели, а затем его выполнение необходимо прервать прежде, чем он решит поставленную задачу. В качестве наиболее простого примера можно привести ситуацию, когда пользователь щелкает мышью на кнопке Отмена, желая Принудительно остановить процесс вычислений. Для того чтобы обеспечить возможность управляемого завершения работы потока, программисту необходимо приложить определенные усилия, но соответствующий механизм, к счастью, прост и надежен. Право на завершение потока запрашивается с помощью вызова метода interrupt, и код соответствующего потока должен сам следить за событием прерывания и отвечать за его выполнение. Рассмотрим пример.

Вызов метода interrupt указывает потоку, что ему следует быть "начеку", в частности позаботиться о завершении своей работы. Впрочем, метод interrupt сам по себе не принуждает поток прекращать свою деятельность, хотя часто прерывает "дремоту" или ожидание потока, выполняющего соответственно функции sleep или wait.

Использование interrupt полезно и оправданно также в тех случаях, когда необходимо обеспечить работающий поток средствами отслеживания происходящих событий. Пусть, например, в цикле обновления экранной информации требуется выполнять обращение к базе данных посредством транзакции и при этом обслуживать интерфейс пользователя (скажем, проверять обращение к кнопке Отмена) после нормального завершения транзакции. В потоке, ответственном за поддержку пользовательского интерфейса, нетрудно предусмотреть обработку события щелчка на кнопке Отмена с обращением к методу interrupt объекта потока, выполняющего обновление данных. Последний, завершая транзакцию, Должен проверить, вызывался ли метод interrupt, и завершить свое выполнение, если событие прерывания действительно произошло.

Перечислим методы, имеющие отношение к механизму прерывания работы потока: (1) interrupt - посылает потоку уведомление о прерывании; (2) SInterrupted - проверяет, была ли прервана работа потока вызовом метода lnterrupt; (3) interrupted - статический метод, проверяющий, выполнялось ли Прерывание текущего потока, и очищающий "состояние прерывания" потока. Последнее может быть очищено только самим потоком - "внешних" способов отмены уведомления о прерывании, посланного потоку, не существует. Вообще говоря, особого смысла в проверке состояния прерывания другого потока попросту нет, поэтому названные методы обычно применяются в контексте текущего Потока. Когда поток обнаруживает посланное уведомление о прерывании, ему

10.8. ЗАВЕРШЕНИЕ ВЫПОЛНЕНИЯ ПОТОКА         271

зачастую необходимо, прежде чем отреагировать на сигнал, выполнить определенные вспомогательные операции. Такие операции могут быть сопряжены с действиями, на которые оказывает влияние необработанное состояние прерывания, поэтому потоки, обнаруживая уведомление прерывания, очищают состояние с помощью метода interrupted.

Прерывание посредством метода interrupt обычно не воздействует на работоспособность потока, но некоторые методы, такие как sleep и wait, будучи прерванными, выбрасывают исключение типа InterruptedException. Другими словами если поток в момент прерывания его работы с помощью interrupt выполняет один из этих методов, они генерируют исключение InterruptedException. В этом случае состояние прерывания потока очищается, поэтому код, обрабатывающий исключение InterruptedException, обычно должен выглядеть следующим образом:

void tick(int count, long pauseTime) {

try {

for (int i = о; i < count; i++) {

System.out.println('. ');

System.out.f1ush();

Thread.sleep(pauseTime);

}

} catch (InterruptedException е) {

Thread.currentThread().interrupt();

}

}

Метод tick выводит на экран символ точки count раз, "засыпая" после каждой операции на период времени, равный значению pauseTime, выраженному в миллисекундах (за дополнительными сведениями о функциях таймеров обращайтесь к разделу 17.5 на странице 487). Если работа потока прерывается посредством interrupt в момент выполнения им метода tick, метод slеер выбрасывает исключение типа InterruptedException. Управление передается из цикла for в предложение catch, где уведомление о прерывании потока посылается заново. Можно, разумеется, упомянуть исключение InterruptedExceptionв предложении throws объявления самого метода tick и просто позволить методу передать ответственность за обработку исключения вовне, но тогда ту же работу придется делать каждому методу, откуда tick вызывается. Повторение вызова прерывания в предложении catch позволяет методу tick при необходимости выполнить собственные завершающие операции и затем позволить остальному коду потока обработать событие прерывания надлежащим образом.

Вообще говоря, любой метод, выполняющий операции блокирования (непосредственно или косвенно), обязан предусмотреть возможность прерывания такой операции посредством метода interrupt и выбрасывания соответствующего исключения, если прерывание действительно произошло. Именно таким образом и действуют методы sleep и wait. В некоторых системах блокирование операций ввода-вывода приводит к выбрасыванию исключения типа InterruptedIOException, являющегося производным классом от базового IOException, объекты которого способны генерироваться большинством методов ввода-вывода (за подробными сведениями обращайтесь к главе 15). Даже если в ходе выполнения операции ввода-вывода система не в состоянии обеспечить реакцию на уведомление о прерывании, она может про верить наличие сигнала о событии прерывания перед началом операции и сгенерировать исключение. Cле-

272         ГЛАВА 10. ПОТОКИ ВЫЧИСЛЕНИЙ

довательно, прерываемый поток должен очистить состояние прерывания, если ему необходимо выполнить ввод-вывод как часть завершающего процесса. Однако  полагать, что interrupt: разблокирует поток, выполняющий операцию ввода вывода,  вообще говоря, нельзя.

Каждый метод любого класса выполняется как часть некоторого потока вычислений, но характеристики поведения метода, как правило, относятся к состоянию соответствующего объекта, но не к состоянию потока, вызвавшего метод. Раз так, резонно спросить, каким образом следует писать текст методов, чтобы позволить потокам реагировать на уведомления о прерывании и обеспечить их средствами управляемого завершения своей работы? Если метод способен блокировать объект, он должен и отвечать на сигналы о прерывании таким образом, как мы только что рассказали. В противном случае вы должны решить, что именно значит для конкретного метода уведомление о прерывании, и оговорить в контракте метода характеристики его поведения в подобных ситуациях. Впрочем, в большинстве случаев методам вообще нет нужды заботиться об обработке уведомлений о прерывании. Золотое правило состоит в том, что никогда не следует скрывать сигнал, посланный методом interrupt, явно очищая состояние прерывания либо отлавливая исключение InterruptedException и никак на него не реагируя, - в этом случае выполнение соответствующего потока прервать не удастся.

Механизм прерывания - это инструмент, позволяющий повысить эффективность "дружелюбного" и аккуратного многопоточного кода. Конечно, если код по своей сути агрессивен или злонамерен, тут уж не поможет ни механизм прерывания, ни какие бы то ни было иные средства организации вычислений.

10.8.2. Ожидание завершения работы потока

Поток способен ждать завершения работы другого потока, используя одну из разновидностей метода join. В простейшем случае предусматривается использование внутреннего бесконечного цикла ожидания. Рассмотрим пример.

class CalcThread extends Thread {

private double result;

public void run() { result = calculate();

}

public double getResult() { return result;

}

public double calculate() {

// ... Вычислить значение поля result

}

}

class ShowJoin {

public static void main(String[] args) {

CalcThread сalc = new CalcThread();

calc.start() ;

dosomethingElse();

10.8. ЗАВЕРШЕНИЕ ВЫПОЛНЕНИЯ ПОТОКА         273

try { calc.join();

sуstет.оut.println("значение result равно"

+ calc.getResult());

} catch (InterruptedException е) { sуstет.оut. println ("результата нет: работа потока прервана");

}

}

static void dosomethingElse() {

/* ... */

}

}

ачала определяется новый производный класс потока, СalcThread, осуществляющий некоторые вычисления. Метод main класса ShowJoin создает и запускает в выполнение экземпляр СalcThread, делает какую-то другую работу )somethingElse), а затем объединяет текущий поток с потоком типа СalcThread. Выход из join определенно означает, что работа метода СalcThread. run завершена и значение result может быть использовано в текущем токе. Если к моменту завершения dosomethingElse поток СalcThread также закончил работу, метод join немедленно возвращает управление. Когда поток по той и иной причине "умирает", соответствующий объект Thread продолжает существование и состояние потока все еще может быть проверено.

В двух других перегруженных вариантах jоin предусматривается задание параметра периода ожидания по аналогии с методами wait. Ниже приведено описание всех трех форм метода join.

public final void join(long millis)

throws InterruptedException

Ожидает завершения выполнения потока или истечения заданного периода времени, выраженного в миллисекундах, в зависимости от того, что произойдет раньше. Нулевое значение параметра означает задание бесконечного промежутка времени. Если работа потока прерывается в момент ожидания, выбрасывается исключение типа InterruptedException.

public final void join(long millis, int nanos)

throws InterruptedException

Более чувствительная версия метода. Величина интервала ожидания складывается из двух составляющих: (выраженной в миллисекундах) и nanos (в наносекундах). Вновь, суммарное нулевое значение параметра, бесконечное ожидание. Величина nanos должна находиться в промежутке 0-999999.

public final void join() throws InterruptedException Метод аналогичен первому варианту при условии join (0).

Если говорить о внутренней реализации join, методы действуют в терминах isАlive и могут быть представлены в виде следующей конструкции:

whi1e (isAlive())

wait();

предположении, что исполняющая система вызывает notifyALL в том случае, :ли выполнение потока прерывается.

274        ГЛАВА 10. ПОТОКИ ВЫЧИСЛЕНИЙ

10.9. Завершение работы приложения

Каждое приложение начинает работу с одним потоком - под управлением этого тока работает метод main. Если затем приложение не создает других потоков, его выполнение прекращается при выходе из main. Но если в приложении предусмотрено создание дополнительных потоков, что с ними случится при завершении main?

Существуют два вида потоков - пользовательские (user) и потоки-демоны (daemon). Наличие пользовательских потоков сохраняет приложение в работающем состоянии. Когда выполнение последнего из пользовательских потоков завершается, деятельность всех демонов прерывается и приложение финиширует. Прерывание работы демонов схоже с вызовом метода destroy - оно происходит внезапно и не оставляет потокам никаких шансов для выполнения завершающих операций, - поэтому демоны ограничены в выборе функциональных возможностей. Чтобы придать потоку "демоническое" обличие, используется вызов setDaemon(true). Проверить принадлежность потока к категории демонов можно с помощью метода isDaemon. По умолчанию "демонический" статус наследуется потоком от потока-"родителя" в момент создания и после старта не. может быть изменен; попытка вызова setDaemon(true) во время работы потока приводит к выбрасыванию исключения типа IllegalThreadStateException.

Если метод main порождает потоки, им по умолчанию присваивается статус пользовательских. По завершении main приложение продолжает работать до тех пор, пока не финишируют оставшиеся пользовательские потоки. Исходный поток, вообще говоря, ничем не отличается от остальных - просто ему пришлось оказаться первым в конкретной сессии работы программы - и трактуется системой точно так же, как и другие пользовательские потоки. Обычно при проектировании исходному потоку ставится задача дать "жизнь" другим потокам, призванным выполнить конкретные действия, и затем "умереть". Если необходимо, чтобы приложение заканчивало работу вместе с исходным потоком, все другие потоки следует создавать в форме демонов.

Чтобы заставить приложение завершить работу, можно воспользоваться методами ехit из состава классов System или Runtime. Каждый из этих методов прерывает выполнение приложения и действует подобно методу destroy, применяемому к каждому отдельному потоку. Приложение, однако, способно запустить специальные потоки, которые должны выполниться перед завершением его работы (соответствующие методы, позволяющие осуществлять подобные функции, перечислены в разделе 18.3 на странице 504).

Многие классы создают потоки внутри приложения неявным образом. Например, пакет Abstract Window Toolkit (AWT), кратко описанный в главе 20, обеспечивает поддержку графического пользовательского интерфейса, управляемого событиями, и его классы создают специальные потоки, обслуживающие все события интерфейса. В классах пакета Remote Method Invocation (RMI), который рассмотрен в той же главе 20, предусмотрено создание потоков, ответственных за реализацию механизмов удаленного вызова процедур. Одни из потоков формируются в виде демонов, другие относятся к категории пользовательских, так что при использовании подобных классов Приложение способно действовать дольше, чем можно было предположить. В таких Случаях, когда не существует иных способов завершения работы про грамм, наличие методов ехit приобретает особое значение.

10.9. ЗАВЕРШЕНИЕ РАБОТЫ ПРИЛОЖЕНИЯ         275

10.10. volatile

Обращение к любому элементу данных, допускающему изменение, со стороны различных потоков должно выполняться при условии синхронизации кода. использование механизмов синхронизации, однако, сопряжено с накладными расходами и не всегда удобно и целесообразно. Язык гарантирует, что операции чтения и записи любых значений, кроме относящихся к типам long или doublе, всегда выполняются атомарным образом - соответствующая переменная в любой момент времени будет содержать только то значение, которое сохранено определенным потоком, но не некую смесь результатов нескольких различных операций записи. Это значит, например, что доступ к атомарной переменной, в которую записывает данные только один поток и результатом пользуются другие потоки, не нуждается в защите посредством синхронизации кода, поскольку опасность взаимовлияния потоков отсутствует. Впрочем, эти соображения не применимы при реализации общей схемы действий "запросить изменить сохранить данные", когда синхронизировать код необходимо всегда.

Атомарный доступ не .гарантирует, что поток всегда сможет считать самую последнюю версию значения, сохраненного в переменной. Действительно, при отсутствии синхронизации вполне допустима ситуация, когда значение, записанное одним потоком, никогда не попадет в "поле зрения" другого потока. На возможность использования одним потоком значений переменной, записанных другим потоком, оказывает влияние множество различных факторов. Современное многопроцессорное аппаратное обеспечение зачастую демонстрирует странные особенности поведения, когда речь идет об обращении к данным в памяти, допускающим совместное использование. С точки зрения программиста подобные вещи не только странны - подчас они кажутся совершенно ничем не обоснованными и противоречащими исходному замыслу. Указанные особенности проявляются в том случае, если потоки обладают временной (рабочей) областью памяти, где значения переменных сохраняются и откуда считываются. Если не оговорено противное, компилятор способен работать исключительно с рабочей памятью и никогда не обращаться к основной области памяти переменной для чтения или обновления данных. Пусть, например, имеется значение, которое непрерывно отображается потоком, поддерживающим графический вывод, и это значение допускает изменение несинхронизированными методами; тогда код отображения может выглядеть следующим образом:

currentValue = 5;

for (;;) {

display.showValue(currentValue);

Thread.sleep(1000); // Вздремнуть на одну секунду

}

Если метод showValue сам по себе не обладает возможностью изменения значения currentValue, компилятор волен выдвинуть предположение о том, ЧТО внутри цикла for это значение можно трактовать как неизменное, и использовать одну и ту же константу 5 на каждой итерации цикла при вызове showValue. Но если содержимое поля currentValue в ходе выполнения цикла подвержено обновлению посредством других потоков, предположение компилятора окажется неверным.

Преодолеть проблему поможет соответствующим образом оформленный синхронизированный код - теперь компилятор не вправе руководствоваться собст-

276        ГЛАВА 10. ПОТОКИ ВЫЧИСЛЕНИЙ

венным мнением относительно возможностей доступа к переменной и обязан при необходимости записи и считывания ее содержимого пользоваться только основной областью памяти переменной.

В отсутствие средств синхронизации существует альтернативный вариант решения проблемы - объявление поля можно снабдить модификатором volatilе. признак volatilе свидетельствует о том, что значение поля может быть изменено в любой непредсказуемый момент, и не позволяет компилятору пускаться в свободные "рассуждения". Если объявить currentValue как volatilе, компилятор будет вынужден, выполняя каждую итерацию цикла, заново перечитывать значение переменной. Метод, выполняющий считывание содержимого переменной, которая обозначена признаком volatil е, гарантированно возвращает наиболее свежую версию сохраненного в ней значения. Впрочем, если актуальность объектной ссылки гарантируется, это вовсе не значит, что и поля объекта, на который указывает эта ссылка, содержат самую последнюю информацию (признаком volatilе помечена ссылка, но не объект как таковой). Наконец, гарантии атомарности доступа можно распространить и на значения типа long и double, если снабдить их volatilе.

10.11. Управление потоками, безопасность и ThreadGroup

При программировании многопоточных приложений с использованием библиотечных классов полезно организовать взаимосвязанные потоки в группы, что позволит управлять ими как единым целым и накладывать ограничения на способность потоков, принадлежащих группе, выполнять те или иные действия.

Потоки объединяются в группы потоков (thread groups) по соображениям улучшения управляемости и безопасности. Одна группа потоков может принадлежать другой группе, составляя иерархию с основной (системной) группой на верхнем уровне. Потоки, относящиеся к группе, могут управляться единовременно - вы вправе прервать работу сразу всех потоков группы либо установить для них единое максимальное значение приоритета выполнения. Группы потоков могут быть использованы также для определения доменов безопасности. Потоки внутри группы обычно наделены возможностями взаимного влияния, распространяемого и на потоки вложенных групп. Говоря о "влиянии", мы подразумеваем, что вызов любого метода способен воздействовать на характеристики Поведения потока, скажем, изменять его приоритет или осуществлять прерывание. В рамках конкретного приложения, однако, бывает необходимо определить политику безопасности, которая, в частности, должна препятствовать влиянию Потоков на потоки, не при надлежащие текущей группе. Потокам внутри отдельных групп могут быть даны различные права на выполнение тех или иных действий в рамках приложения, таких как операции ввода-вывода.

Вообще говоря, методы, затрагивающие функции безопасности, всегда загодя Проверяются соответствующим менеджером безопасности, установленным в системе. Если менеджер запрещает выполнение какого-либо действия, метод выбрасывает исключение типа SecurityException. По умолчанию при старте приложения менеджер безопасности не подключается. Если код выполняется в контексте другого приложения - как, например, аплет в Web-броузере, - вы не можете быть уверены, что некий менеджер безопасности, как правило, установ-

10.11. УПРАВЛЕНИЕ ПОТОКАМИ, БЕЗОПАСНОСТЬ И THREADGROUP      277

лен и надежно функционирует. К числу действий, затрагивающих безопасность системы, относятся, в частности, операции по созданию потоков, управлению ими, осуществлению ввода-вывода и прерыванию работы приложений. За более подробными сведениями обращайтесь к разделу 18.5 на странице 509.

Каждый поток принадлежит определенной группе потоков. Каждая группа Потоков представляется объектом класса ThreadGroup, ограничивающим параметры Поведения "СВОИХ" потоков и предлагающим посреднические услуги при обращении R ним. Задать признак принадлежности потока группе можно при создании Потока, используя вызов соответствующего конструктора. По умолчанию каждый вновь созданный поток вводится в ту же группу, которой принадлежит поток- "родитель", если только в процесс не вмешается менеджер безопасности. Пусть, например, некоторый код, связанный с обработкой событий в аплете, создает новый поток. Тогда менеджер безопасности способен включить его в группу потоков аплета, но не в системную группу потоков, обрабатывающих события. Когда поток завершает работу, соответствующий объект Thread удаляется из группы и далее может быть передан в распоряжение сборщика мусора, если иные ссылки на него отсутствуют.

Существуют три разновидности конструкторов класса Thread, позволяющих определить принадлежность создаваемого потока тому или иному объекту типа ThreadGroup. Два из них были рассмотрены в разделе 10.2 на странице 247, при обсуждении интерфейса Runnablе, а третий описан ниже.

public Thread(ThreadGroup group, String name)

Создает новый объект потока с заданным именем name, принадлежащий объекту группы потоков group.

Чтобы предотвратить вероятность присваивания созданных потоков произвольным группам (это способно ослабить механизм обеспечения безопасности), указанные конструкторы сами по себе могут выбрасывать исключение типа SecuгityException, если потоку-"родителю" не позволено размещать созданный поток в пределах некоторой группы.

После создания потока признак его принадлежности определенному объекту ThreadGroup изменить уже нельзя. Чтобы получить информацию о том, к какой группе относится поток, следует воспользоваться методом getThreadGroup. Для проверки того, допускает ли поток влияние извне (в том смысле, о котором было сказано выше), может применяться метод checkAccess, который генерирует исключение типа securityException, если делать это запрещено, и просто возвращает управление в противном случае (метод объявлен как void).

Группа потоков может быть группои-демопом (daemon group). Впрочем, это понятие никак не связано с концепцией потоков-демопов. "Демонический" объект ThreadGroup автоматически уничтожается, если он становится пустым. Задание признака принадлежности объекта ThreadGroup к категории группдемонов не имеет отношения к тому, является ли любой из потоков, принадлежащих группе, потоком-демоном. Признак воздействует на поведение группы потоков только в том случае, когда она становится пустой.

Объекты групп могут быть использованы также для задания верхней границы значений приоритетов потоков, относящихся к группе. После вызова метода setMaxpriогity с передачей ему соответствующего наибольшего допустимого значения приоритета любая попытка задания значения, превышающего установленный порог, сводится к повышению приоритета потока только до величины максимального уровня. Вызов метода не воздействует на характеристики суще-

278         ГЛАВА 10. ПОТОКИ ВЫЧИСЛЕНИЙ

ствующих потоков. Чтобы обеспечить "господство" одного определенного потока группы над всеми другими (т.е. гарантировать, что его приоритет будет всегда заведомо выше остальных), достаточно вначале задать в качестве Приоритета этого потока наивысшее требуемое значение, а затем с помощью вызова setMaxpriогity установить верхнюю границу приоритетов остальных Потоков группы. Задаваемый предел применяется также к группе потоков как таковой. любая попытка задать новое наибольшее значение, превышающее текущее, будет сведена к повышению приоритета группы потоков только до ранее установленной величины максимального уровня.

static synchronized void maxThread(Thread thr, int priority)

{

ThreadGroup grp = thr.getThreadGroup();

thr.setPriority(priority);

grp.setMaxPriority(thr.getPriority() - 1);

}

Приведенный выше метод предполагает задание требуемого значения приоритета потока, а затем - максимально допустимого приоритета группы потоков, меньшего, нежели установленный приоритет "главного" потока. Новое значение верхней границы приоритетов для группы на единицу меньше фактического приоритета потока; написать просто priority-1 не достаточно, поскольку ранее заданный верхний порог может ограничить наши возможности. Разумеется, в этом примере мы предполагаем, что ни один из потоков группы не обладает предварительно установленным большим приоритетом.

Класс ThreadGroup поддерживает конструкторы и методы, перечисленные ниже.

public ThreadGroup(String name)

Создает новый объект класса ThreadGroup, принадлежащий той группе потоков, к которой относится и поток-"родитель". Как и в случае объектов потоков, имена групп не используются исполняющей системой непосредственно, но в качестве параметра name имени группы может быть передано значение null.

public ThreadGroup(ThreadGroup parent, String name)

Создает новый объект класса ThreadGroup с указанным именем name в составе "родительской" группы потоков parent. Если в качестве parent передано значение null, выбрасывается исключение типа nullРоinterException.

public final String getName()

Возвращает строку имени текущей группы потоков.

ublic final ThreadGroup getparent()

Возвращает ссылку на объект "родительской" группы потоков либо null l, если такового нет (последнее возможно только для группы потоков верхнего уровня иерархии).

public final void setDaemon(boolean daemon)

Придает текущему объекту группы потоков статус принадлежности к категории групп-демонов.

Public final boolean isDaemon()

Возвращает статус принадлежности текущего объекта группы потоков к категории групп-демонов.

10.11. УПРАВЛЕНИЕ ПОТОКАМИ, БЕЗОПАСНОСТЬ И THREADGROUP      279

public final void setMaxPriority(int maxpri)

Устанавливает верхнюю границу приоритетов выполнения для текущей

группы потоков.

public final int getMaxPriority()

Возвращает ранее заданное значение верхней границы приоритетов выполнения для текущей группы потоков.

public final boolean parentOf(ThreadGroup g)

Проверяет, является ли текущая группа "родительской" по отношению к группе g либо совпадает с группой g.

public final void checkAccess()

Выбрасывает исключение типа SecurityException, если текущему Потоку не позволено воздействовать на параметры группы потоков; в против_ ном случае просто возвращает управление.

public final void destroy()

Уничтожает объект группы потоков. Группа не должна содержать птоков, иначе метод выбрасывает исключение типа IllegalThreadStateException. Если в составе группы имеются другие группы, они также не должны содержать потоков. Не уничтожает объекты потоков, принадлежащих группе.

Содержимое группы потоков можно проверить с помощью двух наборов методов: одни возвращают информацию о потоках, принадлежащих группе, а другие - о вложенных группах.

public int activeCount()

Возвращает приблизительное количество действующих (активных) потоков группы, включая и те потоки, которые принадлежат вложенным группам. Количество нельзя считать точным, поскольку в момент выполнения метода оно может измениться, - одни потоки "умирают", а другие создаются. Поток считается действующим, если метод isAlive соответствующего объекта Thread возвращает значение true.

public int enumerate(Thread[] threadsInGroup, boolean recurse)

Заполняет массив threadsInGroup ссылками на объекты действующих потоков группы, принимая во внимание размер массива, и возвращает количество сохраненных ссылок. Если значение параметра recurse равно false, учитываются только те потоки, которые принадлежат непосредственно текущей группе, а в противном случае - еще и потоки, относящиеся. ко всем вложенным группам. ThreadGroup.enumerate предоставляет возможность управления процессом рекурсивного просмотра иерархии вложенных групп, а метод ThreadGroup. activeCount - нет. Последний позволяет получить достаточно точную оценку размера массива, необходимого для хранения результатов выполнения рекурсивной версии enumerate; но при задании в качестве параметра recurse значения false оценка размера массива Окажется завышенной.

public int enumerate(Thread[] threadsInGroup)

Метод аналогичен предыдущему при условии enumerate(threadsInGroup, true).

public int activeGroupCount()

Подобен методу activeCount, но подсчитывает количество групп, включая вложенные. Термин активный ("active") в данном случае означает су-

280        ГЛАВА 10. ПОТОКИ ВЫЧИСЛЕНИЙ

ществующий и используется только для совместимости с activeCount (понятия неактивной группы просто нет).

public int enumerate(ThreadGroup[] groupsInGroup, boolean recurse) Подобен соответствующему варианту метода enumerate для подсчета потоков, но заполняет массив groupsInGroup ссылками на объекты Вложенных групп потоков.

public int enumerate(ThreadGroup[] groupsInGroup)

Метод аналогичен предыдущему при условии enumerate(groupsInGroup, true) .

Для управления потоками в группе могут использоваться методы объекта ThreadGroup. Обращение к методу interrupt объекта группы приводит к вызову методов interrupt для каждого потока в группе, включая и те, Которые принадлежат вложенным группам. Этот метод представляет собой единственный способ применения объекта ThreadGroup в целях непосредственного воздействия на потоки группы - в ранних реализациях Java существовали и другие, но нынче все они не рекомендуются для использования.

В составе класса Thread существуют два статических метода, позволяющих обрабатывать данные о группе, которой принадлежит текущий поток. Они являются сокращенными вариантами цепочки вызовов - currentThread, getThreadGroup для текущего потока и требуемого метода соответствующего объекта ThreadGroup.

public static int activeCount()

Возвращает количество действующих потоков в группе, к которой относится текущий поток.

public static int enumerate(Thread[] threadsInGroup)

Метод аналогичен вызову enumerate(threadsInGroup) объекта группы, которой принадлежит текущий поток.

В классе ThreadGroup также предусмотрен метод, который вызывается, когда поток "умирает" ввиду возникновения необрабатываемого исключения.

public void uncaughtException(Thread thr, Throwable ехс)

Вызывается, когда поток thr в текущей группе генерирует исключение

ехс, которое далее не обрабатывается.

ЭТОТ вопрос подробно рассмотрен в следующем разделе.

Упражнение 10.9. Напишите метод, принимающий в качестве параметра ссылку на объект группы потоков и создающий поток, который периодически Выводит на экран данные об иерархии потоков и подгрупп потоков внутри заданной группы. Протестируйте метод в составе программы, использующей несколько непродолжительных по времени выполнения потоков, которые принадлежат различным группам.

10.12. ПОТОКИ и исключения

Исключительная ситуация всегда возникает в контексте определенного потока в результате действий, выполняемых потоком, _ например, при осуществлении операции целочисленного деления на нуль или принудительного выбрасыва-

10.12. ПОТОКИ И ИСКЛЮЧЕНИЯ          281

ния объекта исключения. Подобные исключения относятся к категории синхронных и всегда воздействуют только на текущий поток.

Выбрасывание исключения приводит к внезапному прерыванию текущих инструкций программы и далее, по цепочке в стеке вызовов, к прекращению работы соответствующих методов. Если исключение не обрабатывается к моменту завершения выполнения метода run, мы имеем дело с так называемым необрабатываемым исключением (uncaught exception). Когда работа потока, Который встретился с исключительной ситуацией, завершается, объект исключения также прекращает свое существование. Поскольку необрабатываемые исключения обычно свидетельствуют о серьезных проблемах, необходимы некие средства, позволяющие проследить причины их возникновения. С этой целью Исполняющая система вызывает метод uncaughtException объекта ThreadGroup группы, которой принадлежит "пострадавший" поток.

Реализация метода uncaughtException, принятая по умолчанию, предполагает вызов одноименного метода объекта "родительской" группы, если таковая есть, либо, в противном случае, обращение к методу printStackTrace объекта исключения, а также вывод информации об исключении на экран. Если необходимо исследовать необрабатываемые исключения каким-то нетрадиционным способом, метод uncaughtException можно переопределить. Если, например, речь идет о графическом приложении, целесообразно предусмотреть возможность вывода информации трассировки стека вызовов в отдельное окно вместо использования стандартной консоли system.error, которой пользуется метод printStackTrace. Для этого нетрудно написать собственную версию uncaughtException, в которой вывод информации трассировки перенаправляется в окно, создаваемое специально для этой цели.

Если "родительскому" потоку необходима информация о причине аварийного завершения работы дочернего потока, последний должен сохранить ее в таком месте, откуда первый будет способен ее "достать". Размещение вызова метода start объекта потока внутри конструкции try ... catch не позволяет отлавливать исключения, генерируемые в процессе работы нового потока, - такая мера помогает справиться только с теми исключениями, которые выбрасываются методом start как таковым.

10.12.1. stop

В главе 8 мы упоминали о двух известных категориях асинхронных исключений - внутренних ошибках виртуальной машины Java и исключениях, генерируемых при вызове метода Thread.stop, не рекомендованного для применения.

На страницах книги мы не уделяем внимания устаревшим и получившим официальное неодобрение методам, но stop - это случай особый, частично ввиду назначения этого метода, но в большей мере из-за того, что вам наверняка придется встречаться с ситуациями, где он используется.

Вызов метода stop приводит к возникновению в соответствующем потоке асинхронного исключения типа ThreadDeath. Этот тип исключения не отличается ничем особенным - его объекты могут быть отловлены точно так же, как и другие, а если исключение не подвергается обработке, последствия такого бездействия со временем приведут к аварийному завершению работы потока. Исключение типа ThreadDeath может проявиться в любой момент на про-

282         ГЛАВА 10. ПОТОКИ ВЫЧИСЛЕНИЙ

тяжении рабочего цикла потока, но только не при выполнении операции захвата блокировки.

Исходный замысел при создании метода stop состоял в обеспечении способа завершения работы потока контролируемым образом. Выбрасывая исключение, которое вряд ли будет обрабатываться, метод, как предполагалось, позволял бы выполнить необходимые завершающие операции в рамках предложений finallу по мере обратной "раскрутки" стека вызовов потока. Но благие намерения остались нереализованными. Метод, во-первых, не в состоянии заставить любой поток прервать выполнение, поскольку поток способен отловить выброшенное исключение и пренебречь им; следовательно, эта мера как средство противодействия "злокозненному" коду, вообще говоря, неэффективна. Во-вторых, вместо того, чтобы обеспечить контролируемое выполнение завершающих операций, метод в действительности допускает порчу объектов. Если stop вызвать в то время, когда поток пребывает в критической секции, при передаче исключения во внешний код блокировка будет освобождена, но объект может остаться в некорректном состоянии из-за частичного завершения инструкций критической секции. После обнаружения упомянутых серьезных изъянов было рекомендовано отказаться от применения метода stop. Вместо него следует использовать interrupt, позволяющий завершать работу Потоков более цивилизованным образом.

Вторая разновидность метода stop предполагает получение в качестве параметра любого объекта Throwable, который будет выброшен в Виде объекта исключения в соответствующем потоке, - это еще более коварное решение, поскольку оно допускает возможность генерирования "недопустимых" объявляемых исключений в пределах кода, где они на самом деле не объявлены.

10.13. Переменные ThreadLocal

Класс ThreadLocal предоставляет возможность иметь единую логическую переменную, обладающую независимыми значениями в контексте каждого отдельного потока. В составе объекта ThreadLocal есть методы set и get, которые позволяют соответственно присваивать и считывать значения переменной для текущего потока. Хотя это средство, возможно, вам никогда не понадобится, при случае оно способно существенно упростить работу.

Пусть, например, Возникает потребность в универсальном пользовательском объекте, который, однако, следует инициализировать в каждом отдельном потоке. Чтобы сохранить значение пользовательского объекта для каждого потока, Уместно прибегнуть к помощи объекта класса ThreadLocal:

public class Operations {

static class User {

static final User UNKNOWN_USER = new User();

} ;

boolean canChange(user u) { return true; }

private static ThreadLocal users = new ThreadLocal() {

/** в качестве исходного используется значение UNKNOWN_USER */

protected Object initialvalue() {

return User.UNKNOWN_USER;

10.13. ПЕРЕМЕННЫЕ THREADLOCAL          283

 }

} ;

private static User currentuser() {

return (user) users.get();

}

public static void setuser(user'newuser) {

users.set(newuser);

}

public void setValue(int newValue) {

user user т currentUser();

if (!canchange(user))

throw new securityException();

// ... изменить значение .. ,

}

//

}

Статическое поле users содержит переменную типа ThreadLocal, в качестве исходного значения которой в каждом потоке употребляется User. UNKNOWN_USER. Это значение воспроизводится с помощью переопределенной версии метода initiаlValue, который по умолчанию возвращает null. Пользовательский объект ставится в соответствие текущему потоку посредством вызова метода setUser. Информация о ранее определенном пользовательском объекте текущего потока может быть получена с помощью метода currentUser. Как видно из текста метода setValue, данные пользовательского объекта далее находят применение для определения привилегий потока.

Когда поток прекращает существование, значения, установленные для этого потока в переменных Thread Local, недостижимы и могут быть уничтожены сборщиком мусора, если какие-либо ссылки на них отсутствуют.

При создании нового потока в качестве соответствующего исходного значения переменной ThreadLocal используется то, которое возвращается методом initiаlValue. Если необходимо, чтобы новый поток наследовал исходное значение, отвечающее потоку-"родителю", можно обратиться к средствам класса InheгitableThreadLocal, производного от ThreadLocal. Он содержит метод childValue, вызываемый для получения исходного значения переменной для дочернего потока. Методу передается значение переменной, соответствующее потоку-"родителю", а возвращает он значение для дочернего потока. По умолчанию childValue возвращает то же значение, которое получает в виде параметра, но ничто не мешает создать производный класс и переопределить метод, чтобы тот формировал нужный клонированный вариант исходного значения переменной, отвечающего дочернему потоку.

Использование переменных ThreadLocal связано с определенным риском.

Обращаться к ним следует только в том случае, если вы отчетливо представляете особенности применяемой модели многопоточных вычислений. Проблемы возникают, в частности, при обращении к модели пула потоков (thread pool), когда для решения очередной задачи вместо создания нового потока предусматривается повторное использование одного из ранее сформированных потоков. В систе-

284        ГЛАВА 10. ПОТОКИ ВЫЧИСЛЕНИЙ

ме, поддерживающей пул потоков, каждый поток может применяться несколько раз. В этом случае любая переменная ThreadLocal к моменту начала очередного сеанса работы потока вместо требуемого исходного значения, обычно возвращаемого методами initiаlValue или childValue, будет содержать значение, оставшееся после завершения предыдущего сеанса. Если вы создали класс общего назначения, предусматривающий использование переменных ThreadLocal, а кто-то позже решит применить его в системе с пулом потоков, последствия могут быть совершенно неожиданными. Подобная ситуация, например, вполне применима к переменной users из рассмотренного выше класса Ореrations  если объект Ореrations используется несколькими потоками или несколько объектов Ореrations находят применение в контексте одного и того же Потока, понятие "текущий пользовательский объект" легко исказить. Программисты, обращающиеся к классу Operations, должны ясно это понимать и Использовать объекты Ореrations только в таких многопоточных средах, которым отвечает контракт класса.

10.14. Отладка потоков

В составе класса Thread есть несколько методов, способных помочь в процессе отладки кода многопоточного приложения. Все они используют средства вывода данных на экран и полезны при необходимости исследования состояния объектов программы.

public String toString()

Возвращает строковое представление содержимого объекта потока, включающее его наименование, значение приоритета выполнения и имя соответствующей группы потоков.

public static void dumpStack()

Выводит на консоль System.err данные трассировки для текущего потока.

Существуют также средства отладки на уровне групп потоков. Для просмотра состояния объектов TreadGroup могут быть использованы методы, рассмотренные Ниже.

public String toString()

Возвращает строковое представление содержимого объекта группы потоков, включающее его наименование и значение приоритета выполнения.

public void list()

Выводит на консоль system.out информацию об объекте ThreadGroup, включающую результаты вызовов toString для каждого из потоков, принадлежащих группе, и всех вложенных групп.

  Я сначала сыграю, а потом скажу, что дальше

Майлс Дэвис (Miles Davis)

10.14. ОТЛАДКА ПОТОКОВ           285




1. Защита населения и объектов от Ч С
2. Аще забуду тебе Иерусалиме
3. Программа перевода десятичного числа в двоичную и шестнадцатеричную системы счисления
4. Этапы становления педагогической психологии
5. Человек на войне (по произведению В Быкова Сотников)
6. Концепция современного естествознания
7. Семья ребенка находящаяся в социальнореабилитационном центре как объект коррекционнореабилитационной
8. Тема курсовой работы Приватизация жилых помещений была выбрана неслучайно
9. Источники церковного права
10. методические основы аудиторской деятельности