Конспект лекций по курсу Выбранные вопросы информатики (часть 2) для специальности Информатика Графика

Вид материалаКонспект

Содержание


Синхронизация потоков
Синхронизация методов
Блокировка потока
Ожидание извещения
Ожидание завершения потока
Аплет Rectangles
Описание исходных текстов аплета Rectangles
Поля класса Rectangles
Метод start класса Rectangles
Метод stop класса Rectangles
Поля класса DrawRectangles
Конструктор класса DrawRectangles
Метод run класса DrawRectangles
Метод run класса DrawEllipse
Поля класса NotifyTask
Конструктор класса NotifyTask
Метод run класса NotifyTask
Подобный материал:
1   ...   6   7   8   9   10   11   12   13   ...   17

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

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

Для чего и когда она нужна?

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

Поясним это на простом примере.

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

на первом шаге проверяется общая сумма денег, которая хранится на счету;

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

значение остатка записывается на текущий счет.

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

Допустим, события разворачиваются следующим образом:

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

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

первый процесс уменьшает счет на 3 млн. долларов и записывает остаток (2 млн. долларов) на текущий счет;

второй процесс выполняет ту же самую операцию, так как после проверки считает, что на счету по-прежнему хранится 5 млн. долларов.

В результате получилось, что со счета, на котором находилось 5 млн. долларов, было снято 6 млн. долларов, и при этом там осталось еще 2 млн. долларов! Итого - банку нанесен ущерб в 3 млн. долларов.

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

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

процесс блокирует счет для выполнения операций другими процессами, получая его в монопольное владение;

процесс проводит процедуру уменьшения счета и записывает на текущий счет новое значение остатка;

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

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

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

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

Синхронизация методов

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

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

public synchronized void decrement()

{

. . .

}

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

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

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

. . .

synchronized(Account)

{

if(Account.check(3000000))

Account.decrement(3000000);

}

. . .

Здесь синхронизация выполняется для объекта Account.

Блокировка потока

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

Блокировка на заданный период времени

С помощью метода sleep можно заблокировать поток на заданный период времени:

try

{

Thread.sleep(500);

}

catch (InterruptedException ee)

{

. . .

}

В данном примере работа потока Thread приостанавливается на 500 миллисекунд. Заметим, что во время ожидания приостановленный поток не отнимает ресурсы процессора.

Так как метод sleep может создавать исключение InterruptedException, необходимо предусмотреть его обработку. Для этого мы использовали операторы try и catch.

Временная приостановка и возобновление работы

Методы suspend и resume позволяют, соответственно, временно приостанавливать и возобновлять работу потока.

В следующем фрагменте кода поток m_Rectangles приостанавливает свою работу, когда курсор мыши оказывается над окном аплета:

public boolean mouseEnter(Event evt,

int x, int y)

{

if (m_Rectangles != null)

{

m_Rectangles.suspend();

}

return true;

}

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

public boolean mouseExit(Event evt,

int x, int y)

{

if (m_Rectangles != null)

{

m_Rectangles.resume();

}

return true;

}

Ожидание извещения

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

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

Как пользоваться методами wait, notify и notifyAll?

Метод, который будет переводиться в состояние ожидания, должен быть синхронизированным, то есть его следует описать как synchronized:

public synchronized void run()

{

while (true)

{

. . .

try

{

this.wait();

}

catch (InterruptedException e)

{

}

}

}

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

Ниже мы привели пример потока, вызывающией метод notify:

public void run()

{

while (true)

{

try

{

Thread.sleep(30);

}

catch (InterruptedException e)

{

}


synchronized(STask)

{

STask.notify();

}

}

}

Этот поток реализован в рамках отдельного класса, конструктору которого передается ссылка на поток, вызывающую метод wait. Эта ссылка хранится в поле STask.

Обратите внимание, что хотя сам метод run не синхронизированный, вызов метода notify выполняется в синхронизированном режиме. В качестве объекта синхронизации выступает поток, для которого вызывается метод notify.

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

С помощью метода join вы можете выполнять ожидание завершения работы потока, для которой этот метод вызван.

Существует три определения метода join:

public final void join();

public final void join(long millis);

public final void join(long millis,

int nanos);

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


Потоки-демоны

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

Заметим, что метод setDaemon необходимо вызывать после создания потока, но до момента его запуска, то есть перед вызовом метода start.

С помощью метода isDaemon вы можете проверить, является поток демоном, или нет.


Аплет Rectangles

В качестве примера многопоточного приложения мы приведем аплет Rectangles (рис. 1). Он создает три потока. Первый поток рисует в окне аплета прямоугольники случайного размера и цвета, второй - эллипсы, а третий управляет потоком рисования эллипсов.



Рис. 1. Окно аплета Rectangles

Расположение прямоугольников и эллипсов также выбирается случайно.


Исходные тексты аплета Rectangles

Исходные тексты аплета Rectangles приведены в листинге 1.

Листинг 1. Файл Rectangles,java

import java.applet.*;

import java.awt.*;

public class Rectangles extends Applet

{

DrawRectangles m_DrawRectThread = null;

DrawEllipse m_DrawEllipseThread = null;

NotifyTask m_NotifyTaskThread = null

public String getAppletInfo()

{

return "Name: Rectangles";

}

public void paint(Graphics g)

{

Dimension dimAppWndDimension = getSize();

g.setColor(Color.yellow);

g.fillRect(0, 0, dimAppWndDimension.width - 1, dimAppWndDimension.height - 1);

g.setColor(Color.black);

g.drawRect(0, 0, dimAppWndDimension.width - 1, dimAppWndDimension.height - 1);

}

public void start()

{

if (m_DrawRectThread == null)

{

m_DrawRectThread = new DrawRectangles(this);

m_DrawRectThread.start();

}

if (m_DrawEllipseThread == null)

{

m_DrawEllipseThread = new DrawEllipse(this);

m_DrawEllipseThread.start();

}

if (m_NotifyTaskThread == null)

{

m_NotifyTaskThread = new NotifyTask(m_DrawEllipseThread);

m_NotifyTaskThread.start();

}

}


public void stop()

{

if (m_DrawRectThread != null)

{

m_DrawRectThread.stop();

m_DrawRectThread = null;

}

if (m_DrawEllipseThread == null)

{

m_DrawEllipseThread.stop();

m_DrawEllipseThread = null;

}

if (m_NotifyTaskThread != null)

{

m_NotifyTaskThread.stop();

m_NotifyTaskThread = null;

}

}

}

class DrawRectangles extends Thread

{

Graphics g;

Dimension dimAppWndDimension;

public DrawRectangles(Applet Appl)

{

g = Appl.getGraphics();

dimAppWndDimension = Appl.getSize();

}

public void run()

{

while (true)

{

int x, y, width, height;

int rColor, gColor, bColor;

x = (int)(dimAppWndDimension.width * Math.random());

y = (int)(dimAppWndDimension.height * Math.random());

width = (int)(dimAppWndDimension.width * Math.random()) / 2;

height = (int)(dimAppWndDimension.height * Math.random()) / 2;

rColor = (int)(255 * Math.random());

gColor = (int)(255 * Math.random());

bColor = (int)(255 * Math.random());

g.setColor(new Color(rColor, gColor, bColor));

g.fillRect(x, y, width, height);

try

{

Thread.sleep(50);

}

catch (InterruptedException e)

{

stop();

}

}

}

}

class DrawEllipse extends Thread

{

Graphics g;

Dimension dimAppWndDimension;

public DrawEllipse(Applet Appl)

{

g = Appl.getGraphics();

dimAppWndDimension = Appl.getSize();

}

public synchronized void run()

{

while (true)

{

int x, y, width, height;

int rColor, gColor, bColor;

x = (int)(dimAppWndDimension.width * Math.random());

y = (int)(dimAppWndDimension.height * Math.random());

width = (int)(dimAppWndDimension.width * Math.random()) / 2;

height = (int)(dimAppWndDimension.height * Math.random()) / 2;

rColor = (int)(255 * Math.random());

gColor = (int)(255 * Math.random());

bColor = (int)(255 * Math.random());

g.setColor(new Color(rColor, gColor, bColor));

g.fillOval(x, y, width, height);

try

{

this.wait();

}

catch (InterruptedException e)

{

}

}

}

}

class NotifyTask extends Thread

{

Thread STask;

public NotifyTask(Thread SynchroTask)

{

STask = SynchroTask;

}

public void run()

{

while (true)

{

try

{

Thread.sleep(30);

}

catch (InterruptedException e)

{

}

synchronized(STask)

{

STask.notify();

}

}

}

}


Описание исходных текстов аплета Rectangles

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

Что же касается основного класса аплета, то он унаследован, как обычно, от класса Applet и не реализует интерфейс Runnable:

public class Rectangles extends Applet

{

. . .

}

Поля класса Rectangles

В классе Rectangles мы определили три поля с именами m_DrawRectThread, m_DrawEllipseThread и m_NotifyTaskThread:

DrawRectangles m_DrawRectThread = null;

DrawEllipse m_DrawEllipseThread = null;

NotifyTask m_NotifyTaskThread = null

Эти поля являются ссылками на классы, соответственно DrawRectangles, DrawEllipse и NotifyTask . Первый из них создан для рисования прямоугольников, второй - эллипсов, а третий - для управления потоком рисования эллипсов.

Указанные поля инициализируются занчением null, что соответствует неработающим или несозданным задачам.

Метод start класса Rectangles

Этот метод последовательно создает три потока и запускает их на выполнение:

if(m_DrawRectThread == null)

{

m_DrawRectThread = new DrawRectangles(this);

m_DrawRectThread.start();

}

if(m_DrawEllipseThread == null)

{

m_DrawEllipseThread = new DrawEllipse(this);

m_DrawEllipseThread.start();

}

if(m_NotifyTaskThread == null)

{

m_NotifyTaskThread = new NotifyTask(m_DrawEllipseThread);

m_NotifyTaskThread.start();

}

В качестве параметра конструкторам классов DrawRectangles и DrawEllipse мы передаем ссылку на аплет Rectangles. Эта ссылка будет нужна для получения контекста отображения и рисования геометрических фигур.

Поток класса NotifyTask будет управлять работой потока DrawEllipse, поэтому мы передаем его конструктору ссылку на соответствующий объект m_DrawEllipseThread.

Метод stop класса Rectangles

Когда пользователь покидает страницу сервера Web с аплетом, метод stop класса Rectangles последовательно останавливает gjnjrb рисования прямоугольников и эллипсов, а также управляющий поток:

if(m_DrawRectThread != null)

{

m_DrawRectThread.stop();

m_DrawRectThread = null;

}

if(m_DrawEllipseThread == null)

{

m_DrawEllipseThread.stop();

m_DrawEllipseThread = null;

}

if(m_NotifyTaskThread != null)

{

m_NotifyTaskThread.stop();

m_NotifyTaskThread = null;

}

Поля класса DrawRectangles

Класс DrawRectangles определен для потока рисования прямоугольников:

class DrawRectangles extends Thread

{

. . .

}

В поле g класа хранится контекст отображения окна аплета, а в поле dimAppWndDimension - размеры этого окна:

Graphics g;

Dimension dimAppWndDimension;

Значения этих полей определяются конструктором класса по ссылке на главный класс аплета.

Конструктор класса DrawRectangles

В качестве параметра конструктору передается ссылка на класс аплета. Конструктор использует эту ссылку для получения и сохранения в полях класса контекста отображения и размеров окна аплета:

public DrawRectangles(Applet Appl)

{

g = Appl.getGraphics();

dimAppWndDimension = Appl.getSize();

}

Метод run класса DrawRectangles

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

Для того чтобы рисовать, необходимо получить контекст отображения. Этот контекст был получен конструктором класса DrawRectangles и может быть использован методом run.

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

В качестве генератора случайных чисел мы используем метод random из класса Math, который при каждом вызове возвращает новое случайное число типа double, лежащее в диапазоне значений от 0.0 до 1.0.

Координаты по осям X и Y рисуемого прямоугольника определяются простым умножением случайного числа, полученного от метода random, соответственно, на ширину и высоту окна аплета:

x = (int)(dimAppWndDimension.width * Math.random());

y = (int)(dimAppWndDimension.height* Math.random());

Аналогично определяются размеры прямоугольника, однако чтобы прямоугольники не были слишком крупными, мы делим полученные значения на 2:

width = (int)(dimAppWndDimension.width * Math.random()) / 2;

height = (int)(dimAppWndDimension.height * Math.random()) / 2;

Так как случайное число имеет тип double, в обоих случаях мы выполняем явное преобразование результата вычислений к типу int.

Для случайного выбора цвета прямоугольника мы вычисляем отдельные цветовые компоненты, умножая значение, полученное от метода random, на число 255:

rColor = (int)(255 * Math.random());

gColor = (int)(255 * Math.random()озникновении указанного исключения работа потока останавливается вызовом метода stop.

Метод run класса DrawEllipse

Класс DrawEllipse очень похож на только что рассмотренный класс DrawRectangles. Отличие есть только в финальном фрагменте метода run, который мы и рассмотрим.

Вместо задержки на 50 миллисекунд метод run из класса DrawEllipse переходит в состояние ожидания извещения, вызывая метод wait:

try

{

this.wait();

}

catch (InterruptedException e)

{

}

Это извещение создается управляющим потоком класса NotifyTask, к описанию которого мы переходим.

Поля класса NotifyTask

В классе NotifyTask мы определили одно поле STask класса Thread. Это поле которое хранит ссылку на поток, работой которого управляет данный класс:

class NotifyTask extends Thread

{

Thread STask;

. . .

}

Конструктор класса NotifyTask

Конструктор класса NotifyTask записывает в поле STask ссылку на задачу рисования эллипсов:

public NotifyTask(Thread SynchroTask)

{

STask = SynchroTask;

}

Метод run класса NotifyTask

Метод run класса NotifyTask периодически разблокирует поток рисования эллипсов, вызывая для этого метод notify в цилке с задержкой 30 миллисекунд. Обращение к объекту STask, который хранит ссылку на поток рисования эллипсов, выполняется с использованием синхронизации:

public void run()

{

while (true)

{

try

{

Thread.sleep(30);

}

catch (InterruptedException e)

{

}

synchronized(STask)

{

STask.notify();

}

}

}

/>
. . .

}

Конструктор класса NotifyTask

Конструктор класса NotifyTask записывает в поле STask ссылку на задачу рисования эллипсов:

public NotifyTask(Thread SynchroTask)

{

STask = SynchroTask;

}

Метод run класса NotifyTask

Метод run класса NotifyTask периодически разблокирует поток рисования эллипсов, вызывая для этого метод notify в цилке с задержкой 30 миллисекунд. Обращение к объекту STask, который хранит ссылку на поток рисования эллипсов, выполняется с использованием синхронизации:

public void run()

{

while (true)

{

try

{

Thread.sleep(30);

}

catch (InterruptedException e)

{

}

synchronized(STask)

{

STask.notify();

}

}

}