Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Секреты программирования для Internet на Java

.pdf
Скачиваний:
181
Добавлен:
02.05.2014
Размер:
3.59 Mб
Скачать

outputThread(String name) { super(name);

}

public void run() {

for(int i=0; i < 3; i++) { System.out.println(getName()); Thread.yield();

}

}

}

class slicerThread extends Thread { slicerThread() {

setPriority(Thread.MAX_PRIORITY);

}

public void run() { while(true) {

try { Thread.sleep(10);

}

catch (InterruptedException ignore) {

}

}

}

}

class runThreads {

public static void main(String argv[]) { slicerThread ts = new slicerThread();

outputThread t1 = new outputThread("Thread 1"); outputThread t2 = new outputThread("Thread 2"); outputThread t3 = new outputThread("Thread 3"); t1.start();

t2.start();

t3.start();

ts.start();

}

}

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

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

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

Группирование потоков

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

Ⱦɚɧɧɚɹ ɜɟɪɫɢɹ ɤɧɢɝɢ ɜɵɩɭɳɟɧɚ ɷɥɟɤɬɪɨɧɧɵɦ ɢɡɞɚɬɟɥɶɫɬɜɨɦ %RRNV VKRS Ɋɚɫɩɪɨɫɬɪɚɧɟɧɢɟ ɩɪɨɞɚɠɚ ɩɟɪɟɡɚɩɢɫɶ ɞɚɧɧɨɣ ɤɧɢɝɢ ɢɥɢ ɟɟ ɱɚɫɬɟɣ ɁȺɉɊȿɓȿɇɕ Ɉ ɜɫɟɯ ɧɚɪɭɲɟɧɢɹɯ ɩɪɨɫɶɛɚ ɫɨɨɛɳɚɬɶ ɩɨ ɚɞɪɟɫɭ piracy@books-shop.com

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

Потоки группируются иерархически. Каждая группа может содержать неограниченное число потоков. Вы можете обращаться к каждому потоку и выполнять операции типа suspend и stop с целыми группами потоков. Давайте создадим несколько групп потоков:

ThreadGroup parent = new ThreadGroup( "parent");

ThreadGroup child = new ThreadGroup ( parent, "child");

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

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

Thread t1 = new Thread(parent);

Thread t2 = new Thread ( child, "t2");

Теперь, когда у нас есть некоторые потоки в различных группах, что мы с ними можем делать? Наиболее полезные методы, используемые с группами потоков, - suspend, resume и stop. Каждый поток в ThreadGroup будет иметь соответствующий вызываемый метод. ThreadGroup воздействует и на потомков. Используя эти методы, мы можем легко выполнить операции с большим числом потоков.

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

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

Синхронизация потоков

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

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

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

Мониторы: защита общедоступной переменной

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

www.books-shop.com

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

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

Пример 11-7a. Вокзал: никакого вмешательства. class Display {

int switch1Loc=15; int switch2Loc=10; int begin1, begin2; int end1, end2;

public void showLoc(String train, int seq, int len) { if (train.compareTo("train1") == 0) {

begin1=seq; end1=seq + len - 1;

if (seq > switch1Loc) { System.out.println("train1 near switch @

"+seq);

}

else if (seq + len > switch2Loc) { System.out.println("train1 @ " + begin1);

}

}

if (train.compareTo("train2") == 0) { begin2=seq;

end2=seq + len - 1;

if (seq > switch1Loc) { System.out.println("train2 near switch @

"+seq);

}

else if (seq + len > switch2Loc) { System.out.println("train2 @ " + begin2);

}

}

// проверка на пересечение

if ((begin1 <= switch1Loc && end1 >= switch2Loc) && (begin2 <= switch1Loc && end2 >= switch2Loc) && (begin1 <= end2) && (begin1 >= begin2)) { System.out.println("CRASH @ " + seq); System.exit(-1);

}

}

}

class train1 extends Thread { int seq=20;

int switch1Loc=15; int switch2Loc=15; int trainLen=3;

Display display;

train1(String name, Display display) { super(name);

this.display = display;

}

public void step() { seq--;

}

public void run() { while(seq > 0) {

www.books-shop.com

step();

display.showLoc(getName(),seq,trainLen);

yield();

}

System.out.println(getName() + " finished");

}

}

Класс train1 - наша первая попытка создать объект "поезд". Каждый поезд обслуживается собственным потоком. Выполненный метод вызовет два метода обеспечения step и showLoc. Метод step используется, чтобы переместиться на некоторое расстояние вперед. В нашем примере мы сделали вокзал пять шагов в длину. На любой стороне мы контролируем действия поезда на пять шагов.

Класс Display используется, чтобы отобразить движение наших поездов. Он обнаружит столкновение, если поезда находятся в одном и том же месте. Эту проверку он делает только в то время, когда поезда находятся на станции. Для удобства мы включили код для класса Display.

Пример 11-7b. Код модели вокзала. class testTrains1 {

public static void main(String argv[]) { Display display = new Display();

train1 t1 = new train1("train1",display); train1 t2 = new train1("train2",display); t1.start();

t2.start();

}

}

Код теста прост, он создает два новых поезда и начинает их передвижение. Вот вывод нашей первой попытки:

train1 near switch @ 14 train2 near switch @ 14 train1 near switch @ 13 train2 near switch @ 13 train1 near switch @ 12 train2 near switch @ 12 train1 near switch @ 11 train2 near switch @ 11 train1 @ 10

train2 @ 10 CRASH @ 10

Заголовки газет кричат о крушении, и программист уволен за некомпетентность. Адвокаты угрожают исками. Компания должна немедленно исправить программы.

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

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

Можно создать класс trainSwitch, который защитит наш переключатель. В нем будут два метода - lock (блокировка) и unlock (разблокировка). Въезжая на опасный путь, поезд должен вызывать lock. Как только поезд оставляет путь, он вызывает unlock. Все другие поезда перед въездом на этот путь должны ждать, пока он не будет разблокирован.

В Java критические разделы кода помечаются ключевым словом synchronyzed. Любой блок кода можно пометить как синхронизированный, но обычно так помечаются методы. Пометка части метода как синхронизированого вообще-то является примером плохого программирования.

www.books-shop.com

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

Итак, к чему приводит пометка метода как синхронизированного? С каждым классом связан один монитор. Когда вызывается синхронизированный метод, он проверяет монитор. Если класс уже блокирован, подпрограмма вызова будет ждать. Это означает, что в один момент времени только один поток будет находиться в критическом блоке. Давайте посмотрим, как это реализуется.

Пример 11-7c. Класс trainSwitch.

class trainSwitch extends Thread { protected boolean inUse=false; public synchronized void lock() {

while (inUse) { try wait();

catch (InterruptedException e);

}

inUse = true;

}

public synchronized void unlock() { inUse = false;

notify();

}

}

Вклассе trainSwitch есть два синхронизированных метода, lock и unlock, которые защищают переменную inUse этого класса. Мы хотим удостовериться, что в любой момент времени только один поток использует переключатель. Состояние переключателя задается в булевской переменной. Когда поезд использует переключатель, inUse устанавливается в true. Любым другим поездам, желающим использовать переключатель, придется ждать, пока булевская переменная не будет сброшена в false.

Впримере 11-7b использовалось два новых метода, wait и notify. Они служат двум целям. Первая их задача - разрешать сложную ситуацию. Что случилось, если бы в вышеприведенном примере оператор wait отсутствовал? Подпрограмма блокировки находилась бы в цикле и занимала монитор для класса. И тогда никто бы не смог вызвать метод разблокировки, которому также нужен монитор.

Методы wait и notify используются, чтобы решить эту проблему. Метод wait заставляет поток ожидать некоторого события. Тем временем монитор освобождается. Это позволяет выполнять другие подпрограммы, которым нужен монитор класса. Когда управление возвращается из метода wait, монитор также восстанавливается. Остальная часть критического раздела все еще защищена.

СОВЕТ Методы wait и notify не являются частью класса Thread. Фактически они входят в класс java.lang.Object. Метод wait() ждет неопределенное количество времени, wait(long) ждет некоторое число миллисекунд и wait(long, int) ждет некоторое число миллисекунд плюс некоторое число наносекунд.

Давайте рассмотрим остальную часть кода для нашего примера.

Пример 11-7d. Исправленная модель вокзала. class train2 extends Thread {

int seq=15;

int switch1Loc=10; int switch2Loc=5; int trainLen=3; Display display; trainSwitch ts;

train2(String name, Display display, trainSwitch ts) { super(name);

this.display = display; this.ts = ts;

www.books-shop.com

}

public void step() {

if (seq == switch1Loc + 1) { // заняли пути

System.out.println("Locking Switch: " +

getName());

ts.lock();

}

else if (seq + trainLen == switch1Loc) { // освободили пути

System.out.println("Unlocking Switch:

"+getName());

ts.unlock();

}

seq--;

}

public void run() { while(seq > 0) {

step();

display.showLoc(getName(),seq,trainLen);

yield();

}

System.out.println(getName() + " safe");

}

}

В этом последнем фрагменте кода мы изменили метод step, используя разработанный нами класс trainSwitch. Когда мы попадаем на переключатель, мы запрашиваем его блокировку. Получив блокировку, мы можем въезжать на пути. Когда конец поезда освободил путь, мы можем разблокировать переключатель. Эти операции гарантируют, что только один поезд находится на пути в один момент времени. Ниже приведен вывод исправленной программы. Обратите внимание, что оба поезда проезжают через переключатель без аварии:

trainl near switch @ 14 traln2 near switch @ 14 tralnl near switch @ 13 train2 near switch @ 13 train1 near switch @ 12 train2 near switch @ 12 train1 near switch @ 11 train2 near switch @ 11 Locking Switch: trainl trainl @ 10

Locking Switch: train2 train1 @ 9

traln1 @ 8 train1 @ 7

Unlocking Switch: trainl trainl @ 6

traln2 @ 10 trainl @ 5 train2 @ 9 trainl @ 4 train2 @ 8 trainl @ 3 train2 @ 7

Unlocking Switch: train2 train2 @ 6

train2 6 5 train2 6 4 train2 @ 3 trainl safe train2 safe

www.books-shop.com

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

Семафоры: защита других общедоступных ресурсов

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

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

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

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

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

Пример 11-8a. Семафор. class Semaphore {

final static int EMPTY = 0; final static int READING = 1; final static int WRITING = 2; protected int state=EMPTY; protected int readCnt=0;

public synchronized void readLock() { if (state == EMPTY) {

state = READING;

}

else if (state == READING) {

}

else if (state == WRITING) { while(state == WRITING) {

try wait();

catch (InterruptedException e);

}

state = READING;

}

readCnt++;

return;

}

public synchronized void writeLock() {

www.books-shop.com

if (state == EMPTY) { state = WRITING;

}

else {

while(state != EMPTY) { try wait();

catch (InterruptedException e);

}

}

}

public synchronized void readUnlock() { readCnt--;

if (readCnt == 0) { state = EMPTY; notify();

}

}

public synchronized void writeUnlock() { state = EMPTY;

notify();

}

}

Класс Semaphore реализует семафор, который мы описали. Он использует методы, объявленные с ключевым словом synchronized, и методы wait и notify. Этот класс может использоваться для защиты общедоступного ресурса. Мы можем теперь использовать этот класс, чтобы защитить наш доступ к файлу. Давайте протестируем наш семафор с помощью нескольких читающих и пишущих потоков.

Пример 11-8b. Тест семафора. class process extends Thread {

String op; Semaphore sem;

process(String name, String op, Semaphore sem) { super(name);

this.op = op; this.sem = sem; start();

}

public void run() {

if (op.compareTo("read") == 0) { System.out.println("Trying to get readLock: " +

getName());

sem.readLock();

System.out.println("Read op: " + getName()); try sleep((int)Math.random() * 50);

catch (InterruptedException e); System.out.println("Unlocking readLock: " + getName()); sem.readUnlock();

}

else if (op.compareTo("write") == 0) { System.out.println("Trying to get writeLock: " +

getName());

sem.writeLock();

System.out.println("Write op: " + getName()); try sleep((int)Math.random() * 50);

catch (InterruptedException e); System.out.println("Unlocking writeLock: " +

getName());

sem.writeUnlock();

}

}

}

class testSem {

www.books-shop.com

public static void main(String argv[]) { Semaphore lock = new Semaphore(); new process("1", "read", lock); new process("2", "read", lock); new process("3", "write", lock); new process("4", "read", lock);

}

}

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

Trying to get readLock: I

Read op: I

Trying to get readLock: 2

Read op: 2

Trying to get writeLock: 3

Trying to get readLock: 4

Read op: 4

Unlocking readLock: I

Unlocking readLock: 2

Unlocking readLock: 4

Write op: 3

Unlocking writeLock: 3

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

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

Семафоры и управление ресурсами - сложные темы. Большинству программистов не придется иметь с ними дела. Но вы можете сопоставить эти проблемы с проблемами множественных потоков.

Предотвращение тупиков

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

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

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

Но как это влияет на программирование на Java? Замените людей на тротуаре потоками. Скажем, у нас есть два защищенных ресурса file1 и file2. Предположим, что для завершения задачи поток нуждается в обоих ресурсах. Первый поток захватывает file1 для себя. В то же самое время второй поток захватывает file2. Первый поток теперь пробует захватить file2, но не

www.books-shop.com

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

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

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

Если все это выглядит сложно, то только потому, что это действительно сложно. Если вы собираетесь экспериментировать с многократными ресурсами, приготовьтесь почитать некоторые учебники. Позвольте предложить один совет: избегайте необходимости блокировать многократные ресурсы. Это то место, откуда начинаются проблемы. Чем большее количество ресурсов требуется каждому потоку, тем сложнее становится проблема.

Переменные volatile

Заключительная тема этой главы - модификатор переменных volatile, с которым большинству программистов сталкиваться не придется. Давайте прочитаем официальное определение языка Java и затем попробуем понять его смысл: "переменная, объявленная с модификатором volatile, как известно, изменяется асинхронно. Компилятор предупрежден, что использовать такие переменные надо более тщательно". Чтобы понять использование наречия "тщательно" в этом контексте, у вас должно быть хорошее понимание виртуальной машины Java. Это в основном означает, что переменная будет переопределена для каждой ссылки. Java причудливо кэширует переменные для многократных потоков, и модификатор сообщает Java-машине прекратить это.

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

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

Что дальше?

Теперь вы должны чувствовать себя вполне уверенно, работая с потоками в Java. Мы рассказали о некоторых наиболее сложных проблемах, возникающих при программировании на Java. В следующей главе мы обсудим, как создать Java-программы, которые преодолеют некоторые из ограничений апплетов. Хотя программы, которые мы разработаем, не так переносимы, как апплеты, вы сможете обратиться к файловой системе и интегрировать в свои программы код на языке C.

ситуациях, у нас был бы выход. К сожалению, современное состояние техники пока не достигло такого уровня.

www.books-shop.com