Аплет Rectangles
В качестве примера многопоточного приложения мы приведем аплет Rectangles (рис. 1). Он создает три потока. Первый поток рисует в окне аплета прямоугольники случайного размера и цвета, второй - эллипсы, а третий управляет потоком рисования эллипсов.
Рис. 1. Окно аплета Rectangles
Расположение прямоугольников и эллипсов также выбирается случайно.
Блокировка на заданный период времени
С помощью метода sleep можно заблокировать поток на заданный период времени:
try { Thread.sleep(500); } catch (InterruptedException ee) { . . . }
В данном примере работа потока Thread приостанавливается на 500 миллисекунд. Заметим, что во время ожидания приостановленный поток не отнимает ресурсы процессора.
Так как метод sleep может создавать исключение InterruptedException, необходимо предусмотреть его обработку. Для этого мы использовали операторы try и catch.
Блокировка потока
Синхронизированный поток, определенный как метод типа synchronized, может переходить в заблокированное состояние автоматически при попытке обращения к ресурсу, занятому другим синхронизированным методом, либо при выполнении некоторых операций ввода или вывода. Однако в ряде случаев полезно иметь более тонкие средства синхронизации, допускающие явное использование по запросу приложения.
Исходные тексты аплета 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(); } } } }
Конструктор класса DrawRectangles
В качестве параметра конструктору передается ссылка на класс аплета. Конструктор использует эту ссылку для получения и сохранения в полях класса контекста отображения и размеров окна аплета:
public DrawRectangles(Applet Appl) { g = Appl.getGraphics(); dimAppWndDimension = Appl.getSize(); }
Конструктор класса NotifyTask
Конструктор класса NotifyTask записывает в поле STask ссылку на задачу рисования эллипсов:
public NotifyTask(Thread SynchroTask) { STask = SynchroTask; }
Конструкторы
Создание нового объекта Thread
public Thread();
Создвание нового объекта Thread с указанием объекта, для которого будет вызываться метод run
public Thread(Runnable target);
Аналогично предыдущему, но дополнительно задается имя нового объекта Thread
public Thread(Runnable target, String name);
Создание объекта Thread с указанием его имени
public Thread(String name);
Создание нового объекта Thread с указанием группы потока и объекта, для которого вызывается метод run
public Thread(ThreadGroup group, Runnable target);
Аналогично предыдущему, но дополнительно задается имя нового объекта Thread
public Thread(ThreadGroup group, Runnable target, String name);
Создание нового объекта Thread с указанием группы потока и имени объекта
public Thread(ThreadGroup group, String name);
Метод run класса DrawEllipse
Класс DrawEllipse очень похож на только что рассмотренный класс DrawRectangles. Отличие есть только в финальном фрагменте метода run, который мы и рассмотрим.
Вместо задержки на 50 миллисекунд метод run из класса DrawEllipse переходит в состояние ожидания извещения, вызывая метод wait:
try { this.wait(); } catch (InterruptedException e) { }
Это извещение создается управляющим потоком класса NotifyTask, к описанию которого мы переходим.
Метод 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()); bColor = (int)(255 * Math.random());
Полученные значения цветовых компонент используются в конструкторе Color для получения цвета. Этот цвет устанавливается в контексте отображения методом setColor:
g.setColor(new Color(rColor, gColor, bColor));
Теперь все готово для рисования прямоугольника, которое мы выполняем при помощи метода fillRect:
g.fillRect(x, y, width, height);
После рисования прямоугольника метод run задерживает свою работу на 50 миллисекунд, вызывая метод sleep:
try { Thread.sleep(50); } catch (InterruptedException e) { stop(); }
Для обработки исключения InterruptedException, которое может возникнуть во время работы этого метода, мы предусмотрели блок try - catch. При возникновении указанного исключения работа потока останавливается вызовом метода stop.
Метод run класса NotifyTask
Метод run класса NotifyTask периодически разблокирует поток рисования эллипсов, вызывая для этого метод notify в цилке с задержкой 30 миллисекунд. Обращение к объекту STask, который хранит ссылку на поток рисования эллипсов, выполняется с использованием синхронизации:
public void run() { while (true) { try { Thread.sleep(30); } catch (InterruptedException e) { }
synchronized(STask) { STask.notify(); } } }
Метод 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; }
Методы
activeCount
Текущее количество активных потоков в группе, к которой принадлежит поток
public static int activeCount();
checkAccess
Текущему потоку разрешается изменять объект Thread
public void checkAccesss();
countStackFrames
Определение количества фреймов в стеке
public int countStackFrames();
currentThread
Определение текущего работающего потока
public static Thread currentThread();
destroy
Принудительное завершение работы потока
public void destroy();
dumpStack
Вывод текущего содержимого стека для отладки
public static void dumpStack();
enumerate
Получение всех объектов Tread данной группы
public static int enumerate(Thread tarray[]);
getName
Определение имени потока
public final String getName();
getPriority
Определение текущего приоритета потока
public final int getPriority();
getThreadGroup
Определение группы, к которой принадлежит поток
public final ThreadGroup getThreadGroup();
interrupt
Прерывание потока
public void interrupt();
interrupted
Определение, является ли поток прерванным
public static boolean interrupted();
isAlive
Определение, выполняется поток или нет
public final boolean isAlive();
isDaemon
Определение, является ли поток демоном
public final boolean isDaemon();
isInterrupted
Определение, является ли поток прерванным
public boolean isInterrupted();
join
Ожидание завершения потока
public final void join();
Ожидание завершения потока в течение заданного времени. Время задается в миллисекундах
public final void join(long millis);
Ожидание завершения потока в течение заданного времени. Время задается в миллисекундах и наносекундах
public final void join(long millis, int nanos);
resume
Запуск временно приостановленного потока
public final void resume();
run
Метод вызывается в том случае, если поток был создан как объект с интерфейсом Runnable
public void run();
setDaemon
Установка для потока режима демона
public final void setDaemon(boolean on);
setName
Устаовка имени потока
public final void setName(String name);
setPriority
Установка приоритета потока
public final void setPriority(int newPriority);
sleep
Задержка потока на заднное время. Время задается в миллисекундах и наносекундах
public static void sleep(long millis);
Задержка потока на заднное время. Время задается в миллисекундах и наносекундах
public static void sleep(long millis, int nanos);
start
Запуск потока на выполнение
public void start();
stop
Остановка выполнения потока
public final void stop();
Аварийная остановка выполнения потока с заданным исключением
public final void stop(Throwable obj);
suspend
Приостановка потока
public final void suspend();
toString
Строка, представляющая объект-поток
public String toString();
yield
Приостановка текущего потока для того чтобы управление было передано другому потоку
public static void yield();
Методы класса Thread
В классе Thread определены три поля, несколько конструкторов и большое количество методов, предназначенных для работы с потоками. Ниже мы привели краткое описание полей, конструкторов и методов.
С помощью конструкторов вы можете создавать потоки различными способами, указывая при необходимости для них имя и группу. Имя предназначено для идентификации потока и является необязательным атрибутом. Что же касается групп, то они предназначены для организации защиты потоков друг от друга в рамках одного приложения.
Методы класса Thread предоставляют все необходимые возможности для управления потоками, в том числе для их синхронизации.
Многопоточность
Наверное, сегодня уже нет необходимости объяснять, что такое многопоточность. Все современные операционные системы, такие как Windows 95, Windows NT, OS/2 или UNIX способны работать в многопоточном режиме, повышая общую производительность системы за счет эффективного распараллеливания выполняемых потоков. Пока один поток находится в состоянии ожидания, например, завершения операции обмена данными с медленным периферийным устройством, другой может продолжать выполнять свою работу.
Пользователи уже давно привыкли запускать параллельно несколько приложений для того чтобы делать несколько дел сразу. Пока одно из них занимается, например, печатью документа на принтере или приемом электронной почты из сети Internet, другое может пересчитывать электронную таблицу или выполнять другую полезную работу. При этом сами по себе запускаемые приложения могут работать в рамках одного потока - операционная система сама заботится о распределении времени между всеми запущенными приложениями.
Создавая приложения для операционной системы Windows на языках программирования С или С++, вы могли решать многие задачи, такие как анимация или работа в сети, и без использования многопоточности. Например, для анимации можно было обрабатывать сообщения соответствующим образом настроенного таймера.
Приложениям Java такая методика недоступна, так как в этой среде не предусмотрено способов периодического вызова каких-либо процедур. Поэтому для решения многих задач вам просто не обойтись без многопоточности.
Описание исходных текстов аплета Rectangles
В этом приложении мы создаем на базе класса Thread три класса. Первый из них предназначен для создания потока рисования прямоугольников, второй - для создания потока рисования закрашенных эллипсов, а третий - для управления потоком рисования эллипсов.
Что же касается основного класса аплета, то он унаследован, как обычно, от класса Applet и не реализует интерфейс Runnable:
public class Rectangles extends Applet { . . . }
Ожидание извещения
Если вам нужно организовать взаимодействие потоков таким образом, чтобы один поток управлял работой другого или других потоков, вы можете воспользоваться методами 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 наносекунд. Учтите, что реально вы не сможете указывать время с точностью до наносекунд, так как дискретность системного таймера компьютера намного больше.
Поля
Три статических поля предназначены для назначения приоритетов потокам.
NORM_PRIORITY
Нормальный
public final static int NORM_PRIORITY;
MAX_PRIORITY
Максимальный
public final static int MAX_PRIORITY;
MIN_PRIORITY
Минимальный
public final static int MIN_PRIORITY;
Поля класса DrawRectangles
Класс DrawRectangles определен для потока рисования прямоугольников:
class DrawRectangles extends Thread { . . . }
В поле g класа хранится контекст отображения окна аплета, а в поле dimAppWndDimension - размеры этого окна:
Graphics g; Dimension dimAppWndDimension;
Значения этих полей определяются конструктором класса по ссылке на главный класс аплета.
Поля класса NotifyTask
В классе NotifyTask мы определили одно поле STask класса Thread. Это поле которое хранит ссылку на поток, работой которого управляет данный класс:
class NotifyTask extends Thread { Thread STask; . . . }
Поля класса Rectangles
В классе Rectangles мы определили три поля с именами m_DrawRectThread, m_DrawEllipseThread и m_NotifyTaskThread:
DrawRectangles m_DrawRectThread = null; DrawEllipse m_DrawEllipseThread = null; NotifyTask m_NotifyTaskThread = null
Эти поля являются ссылками на классы, соответственно DrawRectangles, DrawEllipse и NotifyTask . Первый из них создан для рисования прямоугольников, второй - эллипсов, а третий - для управления потоком рисования эллипсов.
Указанные поля инициализируются занчением null, что соответствует неработающим или несозданным задачам.
Поток
Для каждого процесса операционная система создает один главный поток (thread ), который является потоком выполняющихся по очереди команд центрального процессора. При необходимости главный поток может создавать другие потоки, пользуясь для этого программным интерфейсом операционной системы.
Все потоки, созданные процессом, выполняются в адресном пространстве этого процесса и имеют доступ к ресурсам процесса. Однако поток одного процесса не имеет никакого доступа к ресурсам потока другого процесса, так как они работают в разных адресных пространствах. При необходимости организации взаимодействия между процессами или потоками, принадлежащими разным процессам, следует пользоваться системными средствами, специально предназначенными для этого.
Потоки-демоны
Вызвав для потока метод setDaemon, вы превращаете обычную поток в поток-демон. Такой поток работает в фоновом режиме независимо от породившего его потока. Если поток-демон создает другие потоки, то они также станут получат статус потока-демона.
Заметим, что метод setDaemon необходимо вызывать после создания потока, но до момента его запуска, то есть перед вызовом метода start.
С помощью метода isDaemon вы можете проверить, является поток демоном, или нет.
Применение многопоточности для анимации
Одно из наиболее распространенных применений аплетов - это создание анимационных эффектов типа бегущей строки, мерцающих огней или аналогичных, привлекающих внимание пользователя. Для того чтобы достичь такого эффекта, необходим какой либо механизм, позволяющий выполнять перерисовку всего окна аплета или его части периодически с заданным временным интервалом.
Работа аплетов, так же как и обычных приложений операционной системы Windows, основана на обработке событий. Для классического приложения Windows событие - это приход сообщения в функцию окна. Основной класс аплета обрабатывает события, переопределяя те или иные методы базового класса Applet.
Проблема с периодическим обновлением окна аплета возникает из-за того, что в языке Java не предусмотрено никакого механизма для создания генератора событий, способного вызывать какой-либо метод класса аплета с заданным интервалом времени. Вы не можете поступить так, как поступали в этой ситуации, разрабатывая обычные приложения Windows - создать таймер и организовать обработку периодически поступающих от него сообщений WM_TIMER.
Напомним, что перерисовка окна аплета выполняется методом paint, который вызывается виртуальной машиной Java асинхронно по отношению к выполнению другого кода аплета.
Можно ли воспользоваться методом paint для периодической перерисовки окна аплета, организовав в нем, например, бесконечный цикл с задержкой?
К сожалению, так поступать ни в коем случае нельзя. Метод paint после перерисовки окна аплета должен сразу возвратить управление, иначе работа аплета будет заблокирована.
Единственный выход из создавшейся ситуации - создание потока (или нескльких потоков), которые будут выполнять рисование в окне аплета асинхронно по отношению к работе кода аплета. Например, вы можете создать поток, который периодически обновляет окно аплета, вызывая для этого метод repaint, или рисовать из потока непосредственно в окне аплета, получив предварительно для этого окна контекст отображения.
Приоритеты потоков в приложениях Java
Если процесс создал несколько потоков, то все они выполняются параллельно, причем время центрального процессора (или нескольких центральных процессоров в мультипроцессорных системах) распределяется между этими потоками.
Распределением времени центрального процессора занимается специальный модуль операционной системы - планировщик. Планировщик по очереди передает управление отдельным потокам, так что даже в однопроцессорной системе создается полная иллюзия параллельной работы запущенных потоков.
Распределение времени выполняется по прерываниям системного таймера. Поэтому каждому потоку дается определенный интервал времени, в течении которого он находится в активном состоянии.
Заметим, что распределение времени выполняется для потоков, а не для процессов. Потоки, созданные разными процессами, конкурируют между собой за получение процессорного времени.
Каким именно образом?
Приложения Java могут указывать три значения для приоритетов потоков. Это NORM_PRIORITY, MAX_PRIORITY и MIN_PRIORITY.
По умолчанию вновь созданный поток имеет нормальный приоритет NORM_PRIORITY. Если остальные потоки в системе имеют тот же самый приоритет, то все потоки пользуются процессорным времени на равных правах.
При необходимости вы можете повысить или понизить приоритет отдельных потоков, определив для них значение приоритета, соответственно, MAX_PRIORITY или MIN_PRIORITY. Потоки с повышенным приоритетом выполняются в первую очередь, а с пониженным - только при отсутствии готовых к выполнению потоков, имеющих нормальный или повышенный приоритет.
Процесс
Процесс (process) - это объект, который создается операционной системой, когда пользователь запускает приложение. Процессу выделяется отдельное адресное пространство, причем это пространство физически недоступно для других процессов. Процесс может работать с файлами или с каналами связи локальной или глобальной сети. Когда вы запускаете текстовый процессор или программу калькулятора, вы создаете новый процесс.
Процессы, потоки и приоритеты
Прежде чем приступить к разговору о многопоточности, следует уточнить некоторые термины.
Обычно в любой многопоточной операционной системе выделяют такие объекты, как процессы и потоки. Между ними существует большая разница, которую следует четко себе представлять.
Реализация интерфейса Runnable
Описанный выше способ создания потоков как объектов класса Thread или унаследованных от него классов кажется достаточнао естественным. Однако этот способ не единственный. Если вам нужно создать только один поток, работающую одновременно с кодом аплета, проще выбрать второй способ с использованием интерфейса Runnable.
Идея заключается в том, что основной класс аплета, который является дочерним по отношению к классу Applet, дополнительно реализует интерфейс Runnable, как это показано ниже:
public class MultiTask extends Applet implements Runnable { Thread m_MultiTask = null; . . . public void run() { . . . }
public void start() { if (m_MultiTask == null) { m_MultiTask = new Thread(this); m_MultiTask.start(); } }
public void stop() { if (m_MultiTask != null) { m_MultiTask.stop(); m_MultiTask = null; } } }
Внутри класса необходимо определить метод run, который будет выполняться в рамках отдельного потока. При этом можно считать, что код аплета и код метода run работают одновременно как разные потоки.
Для создания потока используется оператор new. Поток создается как объект класса Thread, причем конструктору передается ссылка на класс аплета:
m_MultiTask = new Thread(this);
При этом, когда поток запустится, управление получит метод run, определенный в классе аплета.
Как запустить поток?
Запуск выполняется, как и раньше, методом start. Обычно поток запускается из метода start аплета, когда пользователь отображает страницу сервера Web, содержащую аплет. Остановка потока выполняется методом stop.
Реализация многопоточности в Java
должны воспользоваться классом java.lang.Thread. В этом классе определены все методы, необходимые для создания потоков, управления их состоянием и синхронизации.
Как пользоваться классом Thread?
Есть две возможности.
Во-первых, вы можете создать свой дочерний класс на базе класса Thread. При этом вы должны переопределить метод run. Ваша реализация этого метода будет работать в рамках отдельного потока.
Во-вторых, ваш класс может реализовать интерфейс Runnable. При этом в рамках вашего класса необходимо определить метод run, который будет работать как отдельный поток.
Второй способ особенно удобен в тех случаях, когда ваш класс должен быть унаследован от какого-либо другого класса (например, от класса Applet) и при этом вам нужна многопоточность. Так как в языке программирования Java нет множественного наследования, невозможно создать класс, для которого в качестве родительского будут выступать классы Applet и Thread. В этом случае реализация интерфейса Runnable является единственным способом решения задачи.
Синхронизация методов
Возможность синхронизации как бы встроена в каждый объект, создаваемый приложением Java. Для этого объекты снабжаются защелками, которые могут быть использованы для блокировки потоков, обращающихся к этим объектам.
Чтобы воспользоваться защелками, вы можете объявить соответствующий метод как synchronized, сделав его синхронизированным:
public synchronized void decrement() { . . . }
При вызове синхронизированного метода соответствующий ему объект (в котором он определен) блокируется для использования другими синхронизированными методами. В результате предотвращается одновременная запись двумя методами значений в область памяти, принадлежащую данному объекту.
Использование синхронизированных методов - достаточно простой способ синхронизации потоков, обращающихся к общим критическим ресурсам, наподобие описанного выше банковского счета.
Заметим, что не обязательно синхронизовать весь метод - можно выполнить синхронизацию только критичного фрагмента кода.
. . . synchronized(Account) { if(Account.check(3000000)) Account.decrement(3000000); } . . .
Здесь синхронизация выполняется для объекта Account.
Синхронизация потоков
Многопоточный режим работы открывает новые возможности для программистов, однако за эти возможности приходится расплачиваться усложнением процесса проектирования приложения и отладки. Основная трудность, с которой сталкиваются программисты, никогда не создававшие ранее многопоточные приложения, это синхронизация одновременно работающих потоков.
Для чего и когда она нужна?
Однопоточная программа, такая, например, как программа MS-DOS, при запуске получает в монопольное распоряжение все ресурсы компьютера. Так как в однопоточной системе существует только один процесс, он использует эти ресурсы в той последовательности, которая соответствует логике работы программы. Процессы и потоки, работающие одновременно в многопоточной системе, могут пытаться обращаться одновременно к одним и тем же ресурсам, что может привести к неправильной работе приложений.
Поясним это на простом примере.
Пусть мы создаем программу, выполняющую операции с банковским счетом. Операция снятия некоторой суммы денег со счета может происходить в следующей последовательности:
на первом шаге проверяется общая сумма денег, которая хранится на счету;
если общая сумма равна или превышает размер снимаемой суммы денег, общая сумма уменьшается на необходимую величину;
значение остатка записывается на текущий счет.
Если операция уменьшения текущего счета выполняется в однопоточной системе, то никаких проблем не возникнет. Однако представим себе, что два процесса пытаются одновременно выполнить только что описанную операцию с одним и тем же счетом. Пусть при этом на счету находится 5 млн. долларов, а оба процесса пытаются снять с него по 3 млн. долларов.
Допустим, события разворачиваются следующим образом:
первый процесс проверяет состояние текущего счета и убеждается, что на нем хранится 5 млн. долларов;
второй процесс проверяет состояние текущего счета и также убеждается, что на нем хранится 5 млн. долларов;
первый процесс уменьшает счет на 3 млн. долларов и записывает остаток (2 млн. долларов) на текущий счет;
второй процесс выполняет ту же самую операцию, так как после проверки считает, что на счету по-прежнему хранится 5 млн. долларов.
В результате получилось, что со счета, на котором находилось 5 млн. долларов, было снято 6 млн. долларов, и при этом там осталось еще 2 млн. долларов! Итого - банку нанесен ущерб в 3 млн. долларов.
Как же составить программу уменьшения счета, чтобы она не позволяла вытворять подобное?
Очень просто - на время выполнения операций над счетом одним процессом необходимо запретить доступ к этому счету со стороны других процессов. В этом случае сценарий работы программы должен быть следующим:
процесс блокирует счет для выполнения операций другими процессами, получая его в монопольное владение;
процесс проводит процедуру уменьшения счета и записывает на текущий счет новое значение остатка;
процесс разблокирует счет, разрешая другим процессам выполнение операций.
Когда первый процесс блокирует счет, он становится недоступен другим процессам. Если второй процесс также попытается заблокировать этот же счет, он будет переведен в состояние ожидания. Когда первый процесс уменьшит счет и на нем останется 2 млн. долларов, второй процесс будет разблокирован. Он проверит остаток, убедится, что сумма недостаточна и не будет проводить операцию.
Таким образом, в многопоточной среде необходима синхронизация потоков при обращении к критическим ресурсам. Если над такими ресурсами будут выполняться операции в неправильной последовательности, это приведет к возникновению трудно обнаруживаемых ошибок.
В языке программирования Java предусмотрено несколько средств для синхронизации потоков, которые мы сейчас рассмотрим.
Создание дочернего класса на базе класса Thread
Рассмотрим первый способ реализации многопоточности, основанный на наследовании от класса Thread. При использовании этого способа вы определяете для потока отдельный класс, например, так:
class DrawRectangles extends Thread { . . . public void run() { . . . } }
Здесь определен класс DrawRectangles, который является дочерним по отношению к классу Thread.
Обратите внимание на метод run. Создавая свой класс на базе класса Thread, вы должны всегда определять этот метод, который и будет выполняться в рамках отдельного потока.
Заметим, что метод run не вызывается напрямую никакими другими методами. Он получает управление при запуске потока методом start.
Как это происходит?
Рассмотрим процедуру запуска потока на примере некоторого класса DrawRectangles.
Вначале ваше приложение должно создать объект класса Thread:
public class MultiTask2 extends Applet { Thread m_DrawRectThread = null; . . . public void start() { if (m_DrawRectThread == null) { m_DrawRectThread = new DrawRectangles(this); m_DrawRectThread.start(); } } }
Создание объекта выполняется оператором new в методе start, который получает управление, когда пользователь открывает документ HTML с аплетом. Сразу после создания поток запускается на выполнение, для чего вызывается метод start.
Что касается метода run, то если поток используется для выполнения какой либо периодической работы, то этот метод содержит внутри себя бесконечный цикл. Когда цикл завершается и метод run возвращает управление, поток прекращает свою работу нормальным, не аварийным образом. Для аварийного завершения потока можно использовать метод interrupt.
Остановка работающего потока выполняется методом stop. Обычно остановка всех работающих потоков, созданных аплетом, выполняется методом stop класса аплета:
public void stop() { if (m_DrawRectThread != null) { m_DrawRectThread.stop(); m_DrawRectThread = null; } }
Напомним, что этот метод вызывается, когда пользователь покидает страницу сервера Web, содержащую аплет.
Временная приостановка и возобновление работы
Методы 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; }