1. Мультизадачность

Наверное, сегодня уже нет необходимости объяснять, что такое мультизадачность. Все современные операционные системы, такие как Microsoft Windows 95, Microsoft Windows NT, IBM OS/2 или UNIX способны работать в мультизадачном режиме, повышая общую производительность системы за счет эффективного распараллеливания выполняемых задач. Пока одна задача находится в состоянии ожидания, например, завершения операции обмена данными с медленным периферийным устройством, другая может продолжать выполнять свою работу.

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

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

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

Процессы, задачи и приоритеты

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

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

Процесс

Процесс (process) - это объект, который создается операционной системой, когда пользователь запускает приложение. Процессу выделяется отдельное адресное пространство, причем это пространство физически недоступно для других процессов. Процесс может работать с файлами или с каналами связи локальной или глобальной сети. Когда вы запускаете текстовый процессор Microsoft Word for Windows или программу калькулятора, вы создаете новый процесс.

Задача

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

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

Приоритеты задач в приложениях Java

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

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

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

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

Каким именно образом?

Приложения Java могут указывать три значения для приоритетов задач. Это NORM_PRIORITY, MAX_PRIORITY и MIN_PRIORITY.

По умолчанию вновь созданная задача имеет нормальный приоритет NORM_PRIORITY. Если остальные задачи в системе имеют тот же самый приоритет, то все задачи пользуются процессорным времени на равных правах.

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

Реализация мультизадачности в Java

Для создания мультизадачных приложений Java вы должны воспользоваться классом java.lang.Thread. В этом классе определены все методы, необходимые для создания задач, управления их состоянием и синхронизации.

Как пользоваться классом Thread?

Есть две возможности.

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

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

Как вы скоро увидите, система автоматизированного создания приложений Java, входящая в состав Microsoft Visual J++, пользуется вторым из перечисленных выше способов. Этот способ удобен в тех случаях, когда ваш класс должен быть унаследован от какого-либо другого класса (например, от класса Applet) и при этом вам нужна мультизадачность. Так как в языке программирования Java нет множественного наследования, невозможно создать класс, для которого в качестве родительского будут выступать классы Applet и Thread. В этом случае реализация интерфейса Runnable является единственным способом решения задачи.

Методы класса Thread

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

public class java.lang.Thread
  extends java.lang.Object
  implements java.lang.Runnable
{
  // -----------------------------------------------------
  // Поля
  // -----------------------------------------------------

  // Приоритеты задач
  public final static int NORM_PRIORITY; // нормальный
  public final static int MAX_PRIORITY;  // максимальный
  public final static int MIN_PRIORITY;  // минимальный

  // -----------------------------------------------------
  // Конструкторы
  // -----------------------------------------------------

  // Создание нового объекта 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);

  // -----------------------------------------------------
  // Методы
  // -----------------------------------------------------

  // Текущее количество активных задач в группе, к которой
  // принадлежит задача
  public static int activeCount();

  // Текущей задаче разрешается изменять объект Thread
  public void checkAccess();

  // Определение количества фреймов в стеке
  public int countStackFrames();

  // Определение текущей работающей задачи
  public static Thread currentThread();

  // Принудительное завершение работы задачи
  public void destroy();

  // Вывод текущего содержимого стека для отладки
  public static void dumpStack();

  // Получение всех объектов Tread данной группы
  public static int enumerate(Thread  tarray[]);

  // Определение имени задачи
  public final String getName();

  // Определение текущего приоритета задачи
  public final int getPriority();

  // Определение группы, к которой принадлежит задача
  public final ThreadGroup getThreadGroup();

  // Прерывание задачи
  public void interrupt();

  // Определение, является ли задача прерванной
  public static boolean interrupted();

  // Определение, выполняется задача или нет
  public final boolean isAlive();

  // Определение, является ли задача демоном
  public final boolean isDaemon();

  // Определение, является ли задача прерванной
  public boolean isInterrupted();
  
  // Ожидание завершения задачи
  public final void join();

  // Ожидание завершения задачи в течение заданного времени.
  // Время задается в миллисекундах
  public final void join(long millis);

  // Ожидание завершения задачи в течение заданного времени.
  // Время задается в миллисекундах и наносекундах
  public final void join(long  millis, int  nanos);

  // Запуск временно приостановленной задачи
  public final void resume();

  // Метод вызывается в том случае, если задача была
  // создана как объект с интерфейсом Runnable
  public void run();

  // Установка для задачи режима демона
  public final void setDaemon(boolean on);

  // Устаовка имени задачи
  public final void setName(String name);

  // Установка приоритета задачи
  public final void setPriority(int newPriority);

  // Задержка задачи на заднное время.
  // Время задается в миллисекундах и наносекундах
  public static void sleep(long millis);

  // Задержка задачи на заднное время.
  // Время задается в миллисекундах и наносекундах
  public static void sleep(long millis, int nanos);

  // Запуск задачи на выполнение
  public void start();

  // Остановка выполнения задачи
  public final void stop();

  // Аварийная остановка выполнения задачи с
  // заданным исключением
  public final void stop(Throwable obj);

  // Приостановка задачи 
  public final void suspend();

  // Строка, представляющая объект-задачу
  public String toString();

  // Приостановка текущей задачи для того  чтобы
  // управление было передано другой задаче
  public static void yield();
}

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

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

Создание дочернего класса на базе класса 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, содержащую аплет.

Реализация интерфейса 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.

Применение мультизадачности для анимации

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

Работа аплетов, так же как и обычных приложений операционной системы Microsoft Windows, основана на обработке событий. Для классического приложения Microsoft Windows событие - это приход сообщения в функцию окна. Основной класс аплета обрабатывает события, переопределяя те или иные методы базового класса Applet.

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

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

Можно ли воспользоваться методом paint для периодической перерисовки окна аплета, организовав в нем, например, бесконечный цикл с задержкой?

К сожалению, так поступать ни в коем случае нельзя. Метод paint после перерисовки окна аплета должен сразу возвратить управление, иначе работа аплета будет заблокирована.

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

Приложение MultiTask

Система автоматизированной разработки аплетов Microsoft Visual J++ позволяет указать, что создаваемый аплет будет мультизадачным. Для этого на третьем шаге в поле Would you like your applet to be multi-threaded следует включить переключатель Yes (рис. 1.1).

Рис. 1.1. Добавление мультизадачности в создаваемый аплет

Включив указанный переключатель, выключите пока переключатель Would you like support for animation - анимацией мы займемся немного позже.

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

Исходные тексты приложения

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

Листинг 1.1. Файл MultiTask\MultiTask.java

// =========================================================
// Периодическое обновление окна аплета 
// с использованием мультизадачности
//
// (C) Фролов А.В, 1997
//
// E-mail: frolov@glas.apc.org
// WWW:    http://www.glasnet.ru/~frolov
//            или
//         http://www.dials.ccas.ru/frolov
// =========================================================
import java.applet.*;
import java.awt.*;

public class MultiTask extends Applet implements Runnable
{
  // Задача, которая будет обновлять окно аплета
  Thread m_MultiTask = null;

  // -------------------------------------------------------
  // MultiTask
  // Конструктор класса MultiTask. Не используется
  // -------------------------------------------------------
  public MultiTask()
  {
  }

  // -------------------------------------------------------
  // getAppletInfo
  // Метод, возвращающей строку информации об аплете
  // -------------------------------------------------------
  public String getAppletInfo()
  {
    return "Name: MultiTask\r\n" +
      "E-mail: frolov@glas.apc.org" +
      "WWW:    http://www.glasnet.ru/~frolov" +
      "Author: Alexandr Frolov\r\n" +
      "Created with Microsoft Visual J++ Version 1.0";
   }

  // -------------------------------------------------------
  // init
  // Метод, получающий управление при инициализации аплета
  // -------------------------------------------------------
  public void init()
  {
  }

  // -------------------------------------------------------
  // destroy
  // Метод, получающий управление при 
  // завершении работы аплета
  // -------------------------------------------------------
  public void destroy()
  {
  }

  // -------------------------------------------------------
  // paint
  // Метод paint, выполняющий рисование в окне аплета
  // -------------------------------------------------------
  public void paint(Graphics g)
  {
    // Отображения строки со случайным числом
    g.drawString("Running: " + Math.random(), 10, 20);
  }

  // -------------------------------------------------------
  // start
  // Метод вызывается при первом отображении окна аплета
  // -------------------------------------------------------
  public void start()
  {
    // Если задача еще не была создана, аплет создает
    // новую задачу как объект класса Thread, 
    // а затем запускает ее
    if (m_MultiTask == null)
    {
      // Создание задачи
      m_MultiTask = new Thread(this);

      // Запуск задачи
      m_MultiTask.start();
    }
  }
	
  // -------------------------------------------------------
  // stop
  // Метод вызывается, когда страница с аплетом 
  // исчезает с экрана
  // -------------------------------------------------------
  public void stop()
  {
    // Когда пользователь покидает страницу с аплетом, 
    // метод stop останавливает задачу.
    // Остановка выполняется только в том случае,
    // если задача была создана
    if (m_MultiTask != null)
    {
      // Остановка задачи
      m_MultiTask.stop();

      // Сброс ссылки на задачу
      m_MultiTask = null;
    }
  }

  // -------------------------------------------------------
  // run
  // Метод, который работает в рамках отдельной задачи
  // Он вызывает периодическое обновление содержимого
  // окна аплета
  // -------------------------------------------------------
  public void run()
  {
    // Выполняем обновление окна в бесконечном цикле
    while (true)
    {
      try
      {
        // Вызываем функцию обновления окна
        repaint();

        // Выполняем небольшую задержку
        Thread.sleep(50);
      }
      catch (InterruptedException e)
      {
        // Если при выполнении задержки произошло
        // исключение, останавливаем работу задачи
        stop();
      }
    }
  }
}

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

Листинг 1.2. Файл MultiTask\MultiTask.html

<html>
<head>
<title>MultiTask</title>
</head>
<body>
<hr>
<applet
    code=MultiTask.class
    id=MultiTask
    width=320
    height=240 >
</applet>
<hr>
<a href="MultiTask.java">The source.</a>
</body>
</html>

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

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

public class MultiTask extends Applet implements Runnable
{
  . . .
}

Внутри класса определяется поле с именем m_MultiTask типа Thread, которое предназначено для хранения ссылки на объект класса Thread, то есть на задачу:

Thread m_MultiTask = null;

Поле инициализируется значением null. Реальная ссылка на задачу будет сюда записана только после создания задачи.

Рассмотрим теперь методы класса.

Конструктор MultiTask

В нашем аплете конструктор не используется.

Метод getAppletInfo

Метод getAppletInfo возвращает информацию об аплете.

Метод init

Метод init вызывается один раз при инициализации аплета. Наше приложение его не использует.

Метод destroy

При завершении работы аплета управление передается методу destroy. Мы его не используем.

Метод paint

Метод paint рисует в окне аплета текстовую строку и случайное число, полученное при помощи статического метода random класса Math:

public void paint(Graphics g)
{
  g.drawString("Running: " + Math.random(), 10, 20);
}

Напомним, что в однозадачном приложении метод paint вызывается при первом создании окна аплета, а также в случае необходимости перерисовки этого окна.

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

Метод start

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

public void start()
{
  if(m_MultiTask == null)
  {
    m_MultiTask = new Thread(this);
    m_MultiTask.start();
  }
}

Первоначально в поле m_MultiTask находится значение null, поэтому при первом вызове метода start всегда создается задача как объекта класса Thread. При этом конструктору с помощью ключевого слова this передается ссылка на наш аплет, поэтому при запуске задачи управление будет передано методу run, определенному в аплете.

Созданная задача не запускается автоматически. Для запуска необходимо вызвать метод start.

Метод stop

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

public void stop()
{
  if(m_MultiTask != null)
  {
    m_MultiTask.stop();
    m_MultiTask = null;
  }
}

После остановки мы записываем в поле m_MultiTask значение null.

Метод run

Метод run получает управление при запуске задачи методом start. Если этот метод возвращает управление, соответствующая задача завершает свою работу.

Наша реализация метода run состоит из бесконечного цикла, в котором периодически с задержкой 50 миллисекунд вызывается метод repaint:

public void run()
{
  while(true)
  {
    try
    {
      repaint();
      Thread.sleep(50);
    }
    catch(InterruptedException e)
    {
      stop();
    }
  }
}

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

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

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

Приложение Rectangles

В предыдущем приложении задача выполняла периодическую перерисовку окна аплета, вызывая из метода run метод repaint. Такая методика приводит к сильному мерцанию окна аплета и поэтому во многих случаях нежелательна. Приложение Rectangles, постоянно отображающее в своем окне прямоугольники случайного размера, расположения и цвета (рис. 1.2), использует другой способ. Оно запускает задачу, которая рисует в окне аплета непосредственно.

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

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

Исходные тексты приложения

Исходный файл приложения Rectangles приведен в листинге 1.3.

Листинг 1.3. Файл Rectangles\Rectangles.java

// =========================================================
// Рисование прямоугольников в отдельной задаче
//
// (C) Фролов А.В, 1997
//
// E-mail: frolov@glas.apc.org
// WWW:    http://www.glasnet.ru/~frolov
//            или
//         http://www.dials.ccas.ru/frolov
// =========================================================
import java.applet.*;
import java.awt.*;
import java.util.*;

public class Rectangles extends Applet implements Runnable
{
  // Ссылка на задачу рисования прямоугольников
  Thread m_Rectangles = null;

  // -------------------------------------------------------
  // getAppletInfo
  // Метод, возвращающей строку информации об аплете
  // -------------------------------------------------------
  public String getAppletInfo()
  {
    return "Name: Rectangles\r\n" +
      "Author: Alexandr Frolov\r\n" +
      "E-mail: frolov@glas.apc.org" +
      "WWW:    http://www.glasnet.ru/~frolov" +
      "Created with Microsoft Visual J++ Version 1.0";
  }

  // -------------------------------------------------------
  // paint
  // Метод paint, выполняющий рисование в окне аплета
  // -------------------------------------------------------
  public void paint(Graphics g)
  {
    // Определяем текущие размеры окна аплета
    Dimension dimAppWndDimension = size();
    
    // Выбираем в контекст отображения желтый цвет
    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);
  }

  // -------------------------------------------------------
  // start
  // Метод вызывается при первом отображении окна аплета
  // -------------------------------------------------------
  public void start()
  {
    if (m_Rectangles == null)
    {
      m_Rectangles = new Thread(this);
      m_Rectangles.start();
    }
  }
	
  // -------------------------------------------------------
  // start
  // Метод вызывается при первом отображении окна аплета
  // -------------------------------------------------------
  public void stop()
  {
    if (m_Rectangles != null)
    {
      m_Rectangles.stop();
      m_Rectangles = null;
    }
  }

  // -------------------------------------------------------
  // run
  // Метод, который работает в рамках отдельной задачи
  // Он рисует в окне аплета прямоугольники случайного
  // цвета, размера и расположения
  // -------------------------------------------------------
  public void run()
  {
    // Получаем контекст отображения для окна аплета
    Graphics g = getGraphics();

    // Определяем текущие размеры окна аплета
    Dimension dimAppWndDimension = size();

    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);

      // Выполняем задержку на 50 миллисекунд
      try
	{
	  Thread.sleep(50);
	}
	catch (InterruptedException e)
	{
	  stop();
	}
    }
  }

  // -------------------------------------------------------
  // mouseEnter
  // Метод вызывается, когда курсор мыши оказывается над
  // окном аплета
  // -------------------------------------------------------
  public boolean mouseEnter(Event evt, int x, int y)
  {
    if (m_Rectangles != null)
    {
      // Когда курсор мыши оказывается над поверхностью
      // окна аплета, временно приостанавливаем
      // задачу рисования прямоугольников
      m_Rectangles.suspend();
    }
    return true;
  }

  // -------------------------------------------------------
  // mouseExit
  // Метод вызывается, когда курсор мыши покидает
  // окно аплета
  // -------------------------------------------------------
  public boolean mouseExit(Event evt, int x, int y)
  {
    if (m_Rectangles != null)
    {
      // Когда курсор мыши покидает окно аплета,
      // возобновляем работу задачи рисования 
      // прямоугольников
      m_Rectangles.resume();
    }
    return true;
  }
}

В листинге 1.4 находится исходный текст документа HTML, созданного автоматически для нашего аплета.

Листинг 1.4. Файл Rectangles\Rectangles.html

<html>
<head>
<title>Rectangles</title>
</head>
<body>
<hr>
<applet
    code=Rectangles.class
    id=Rectangles
    width=320
    height=240 >
</applet>
<hr>
<a href="Rectangles.java">The source.</a>
</body>
</html>

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

Для создания задачи аплет Rectangles реализует интерфейс Runnable, то есть использует второй из описанных нами методов, как и предыдущий аплет.

Ниже мы рассмотрим наиболее важные методы аплета Rectangles.

Метод paint

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

Метод start

Когда пользователь начинает просмотр документа HTML, содержащего наш аплет, метод start создает и запускает задачу. Для создания задачи мы используем оператор new, а для старта задачи - метод start класса Thread:

public void start()
{
  if (m_Rectangles == null)
  {
    m_Rectangles = new Thread(this);
    m_Rectangles.start();
  }
}

Обратите внимание, что мы передаем конструктору класса Thread параметр this - ссылку на аплет. В результате роль задачи, работающей параллельно с кодом аплета, будет выполнять метод run, определенный в классе аплета.

Ссылка на созданную задачу записывается в поле m_Rectangles.

Метод stop

Метод stop нашего аплета не имеет никаких особенностей. Он вызывается, когда пользователь покидает страницу сервера Web с аплетом. В этом случае метод останавливает задачу, вызывая для этого метод stop класса Thread:

public void stop()
{
  if (m_Rectangles != null)
  {
    m_Rectangles.stop();
    m_Rectangles = null;
  }
}

После остановки в поле m_Rectangles записывается значение null. Это является признаком того, что задача остановлена.

Метод run

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

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

Graphics g = getGraphics();

Для рисования нам также нужно знать размеры окна аплета. Мы получаем эти размеры при помощи метода size:

Dimension dimAppWndDimension = size();

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

В качестве генератора случайных чисел мы используем метод 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.

Метод mouseEnter

В предыдущем томе “Библиотеки системного программиста” мы рассказывали о методах mouseEnter и mouseExit. Первый из этих методов вызывается, когда в результате перемещения курсор мыши оказывается над окном аплета, а второе - когда курсор покидает окно аплета. Мы переопределили эти методы в своем аплете.

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

public boolean mouseEnter(Event evt, int x, int y)
{
  if (m_Rectangles != null)
  {
    m_Rectangles.suspend();
  }
  return true;
}

Преостановленная задача не уничтожается. Ее работа может быть продолжена с помощью метода resume.

Метод mouseExit

Когда курсор мыши покидает окно аплета, вызывается метод mouseExit. Этот метод в нашем аплете возобновляет работу задачи, временно приостановленной методом suspend. Для этого используется метод resume, как это показано ниже:

public boolean mouseExit(Event evt, int x, int y)
{
  if (m_Rectangles != null)
  {
    m_Rectangles.resume();
  }
  return true;
}

Приложение MultiTask2

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

Рис. 1.3. Окно аплета MultiTask2

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

Исходные тексты приложения

Исходный текст приложения вы найдете в листинге 1.5.

Листинг 1.5. Файл MultiTask2\ MultiTask2.java

// =========================================================
// Рисование прямоугольников и эллипсов
// в разных задачах
//
// (C) Фролов А.В, 1997
//
// E-mail: frolov@glas.apc.org
// WWW:    http://www.glasnet.ru/~frolov
//            или
//         http://www.dials.ccas.ru/frolov
// =========================================================
import java.applet.*;
import java.awt.*;

// =========================================================
// Основной класс аплета
// =========================================================
public class MultiTask2 extends Applet
{
  // Ссылка на задачу рисования прямоугольников
  DrawRectangles m_DrawRectThread = null;

  // Ссылка на задачу рисования эллипсов
  DrawEllipse m_DrawEllipseThread = null;

  // -------------------------------------------------------
  // getAppletInfo
  // Метод, возвращающей строку информации об аплете
  // -------------------------------------------------------
  public String getAppletInfo()
  {
    return "Name: MultiTask2\r\n" +
      "Author: Alexandr Frolov\r\n" +
      "E-mail: frolov@glas.apc.org" +
      "WWW:    http://www.glasnet.ru/~frolov" +
      "Created with Microsoft Visual J++ Version 1.0";
  }

  // -------------------------------------------------------
  // paint
  // Метод paint, выполняющий рисование в окне аплета
  // -------------------------------------------------------
  public void paint(Graphics g)
  {
    // Определяем текущие размеры окна аплета
    Dimension dimAppWndDimension = size();
    
    // Выбираем в контекст отображения желтый цвет
    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);
  }

  // -------------------------------------------------------
  // start
  // Метод вызывается при первом отображении окна аплета
  // -------------------------------------------------------
  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();
    }
  }
	
  // -------------------------------------------------------
  // stop
  // Метод вызывается, когда окно аплета исчезает с экрана
  // -------------------------------------------------------
  public void stop()
  {
    if (m_DrawRectThread != null)
    {
      m_DrawRectThread.stop();
      m_DrawRectThread = null;
    }

    if (m_DrawEllipseThread == null)
    {
      m_DrawEllipseThread.stop();
      m_DrawEllipseThread = null;
    }
  }
}

// =========================================================
// Класс задачи для рисования прямоугольников
// =========================================================
class DrawRectangles extends Thread
{
  // Контекст отображения окна аплета
  Graphics g;

  // Размеры окна аплета
  Dimension dimAppWndDimension;

  // -------------------------------------------------------
  // DrawRectangles
  // Конструктор класса DrawRectangles
  // -------------------------------------------------------
  public DrawRectangles(Applet Appl)
  {
    // Получаем и сохраняем контекст отображения
    g = Appl.getGraphics();

    // Определяем текущие размеры окна аплета
    dimAppWndDimension = Appl.size();
  }

  // -------------------------------------------------------
  // run
  // Метод, который работает в рамках отдельной задачи
  // Он рисует в окне аплета прямоугольники случайного
  // цвета, размера и расположения
  // -------------------------------------------------------
  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);

      // Выполняем задержку на 50 миллисекунд
      try
      {
        Thread.sleep(50);
      }
      catch (InterruptedException e)
      {
        stop();
      }
    }
  }
}

// =========================================================
// Класс задачи для рисования эллипсов
// =========================================================
class DrawEllipse extends Thread
{
  // Контекст отображения окна аплета
  Graphics g;

  // Размеры окна аплета
  Dimension dimAppWndDimension;

  // -------------------------------------------------------
  // DrawEllipse
  // Конструктор класса DrawEllipse
  // -------------------------------------------------------
  public DrawEllipse(Applet Appl)
  {
    g = Appl.getGraphics();

    // Определяем текущие размеры окна аплета
    dimAppWndDimension = Appl.size();
  }

  // -------------------------------------------------------
  // run
  // Метод, который работает в рамках отдельной задачи
  // Он рисует в окне аплета эллипсы случайного
  // цвета, размера и расположения
  // -------------------------------------------------------
  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.fillOval(x, y, width, height);

      // Выполняем задержку на 50 миллисекунд
      try
      {
        Thread.sleep(50);
      }
      catch (InterruptedException e)
      {
        stop();
      }
    }
  }
}

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

Листинг 1.6. Файл MultiTask2\ MultiTask2.html

<html>
<head>
<title>MultiTask2</title>
</head>
<body>
<hr>
<applet
    code=MultiTask2.class
    id=MultiTask2
    width=320
    height=240 >
</applet>
<hr>
<a href="MultiTask2.java">The source.</a>
</body>
</html>

Описание исходного текста

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

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

Поля класса MultiTask2

В классе MultiTask2 мы определили два поля с именами m_DrawRectThread и m_DrawEllipseThread:

DrawRectangles m_DrawRectThread = null;
DrawEllipse m_DrawEllipseThread = null;

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

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

Метод paint класса MultiTask2

Метод paint класса MultiTask2 не делает ничего нового по сравнению с аналогичным методом предыдущего аплета. Он просто раскрашивает окно аплета в желтый цвет и рисует вокруг него черную рамку.

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

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

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();
  }
}

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

Когда пользователь покидает страницу сервера Web с аплетом, метод stop класса MultiTask2 последовательно останавливает задачи рисования прямоугольников и эллипсов:

public void stop()
{
  if (m_DrawRectThread != null)
  {
    m_DrawRectThread.stop();
    m_DrawRectThread = null;
  }
  if (m_DrawEllipseThread == null)
  {
    m_DrawEllipseThread.stop();
    m_DrawEllipseThread = null;
  }
}

Поля класса DrawRectangles

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

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

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

g = Appl.getGraphics();
dimAppWndDimension = Appl.size();

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

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

Класс DrawEllipse

Исходный текст класса DrawEllipse лишь немного отличается от исходного текста класса DrawRectangles. Отличие есть в методе run - этот метод рисует не прямоугольники, а закрашенные эллипсы, вызывая для этого метод fillOval.

Приложение Scroller

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

В своем окне приложение Scroller показывает строки текста, медленно всплывающие вверх (рис. 1.4). Вы можете использовать этот аплет для размещения рекламной информации на своем сервере. Всплывающий текст (как и всякое движение на сранице сервера Web) будет привлекать внимание пользователя.

Рис. 1.4. Окно аплета Scroller

Строки (в количестве 6 штук) можно задавать в параметрах аплета, редактируя текст документа HTML, содержащего этот аплет. Первая строка выделяется красным цветом.

Исходные тексты приложения

Исходный текст приложения Scroller представлен в листинге 1.7.

Листинг 1.7. Файл Scroller\Scroller.java

// =========================================================
// Просмотр текста в режиме динамической свертки 
// по вертикали
//
// (C) Фролов А.В, 1997
//
// E-mail: frolov@glas.apc.org
// WWW:    http://www.glasnet.ru/~frolov
//            или
//         http://www.dials.ccas.ru/frolov
// =========================================================
import java.applet.*;
import java.awt.*;

public class Scroller extends Applet implements Runnable
{
  // Ссылка на задачу, выполняющую свертку
  Thread m_Scroller = null;

  // Отображаемые строки
  private String m_String1 = "Мы представляем наши новые книги";
  private String m_String2 = 
    "Том 29. Сервер Web своими руками";
  private String m_String3 = 
    "Том 30. Microsoft Visual J++. Создание приложений на языке Java. Часть 1";
  private String m_String4 = 
    "Том 31. Разработка приложений для Internet с Visual C++ и MFC";
  private String m_String5 = 
    "Том 32. Microsoft Visual J++. Создание приложений на языке Java. Часть 2";
  private String m_String6 = "";

  // Имена параметров
  private final String PARAM_String1 = "String1";
  private final String PARAM_String2 = "String2";
  private final String PARAM_String3 = "String3";
  private final String PARAM_String4 = "String4";
  private final String PARAM_String5 = "String5";
  private final String PARAM_String6 = "String6";

  // -------------------------------------------------------
  // getAppletInfo
  // Метод, возвращающей строку информации об аплете
  // -------------------------------------------------------
  public String getAppletInfo()
  {
    return "Name: Scroller\r\n" +
      "Author: Alexandr Frolov\r\n" +
      "E-mail: frolov@glas.apc.org" +
      "WWW:    http://www.glasnet.ru/~frolov" +
      "Created with Microsoft Visual J++ Version 1.0";
  }

  // -------------------------------------------------------
  // getParameterInfo
  // Иинформация о параметрах
  // -------------------------------------------------------
  public String[][] getParameterInfo()
  {
    String[][] info =
    {
      { PARAM_String1, "String", "Parameter description" },
      { PARAM_String2, "String", "Parameter description" },
      { PARAM_String3, "String", "Parameter description" },
      { PARAM_String4, "String", "Parameter description" },
      { PARAM_String5, "String", "Parameter description" },
      { PARAM_String6, "String", "Parameter description" },
    };
    return info;		
  }

  // -------------------------------------------------------
  // init
  // Метод, получающий управление при инициализации аплета
  // -------------------------------------------------------
  public void init()
  {
    // Рабочая строка
    String param;

    // Получение параметров
    param = getParameter(PARAM_String1);
    if (param != null)
      m_String1 = param;

    param = getParameter(PARAM_String2);
    if (param != null)
      m_String2 = param;

    param = getParameter(PARAM_String3);
    if (param != null)
      m_String3 = param;

    param = getParameter(PARAM_String4);
    if (param != null)
      m_String4 = param;

    param = getParameter(PARAM_String5);
    if (param != null)
      m_String5 = param;

    param = getParameter(PARAM_String6);
    if (param != null)
      m_String6 = param;
  }

  // -------------------------------------------------------
  // paint
  // Метод paint, выполняющий рисование в окне аплета
  // -------------------------------------------------------
  public void paint(Graphics g)
  {
    // Определяем текущие размеры окна аплета
    Dimension dimAppWndDimension = size();

    // Определяем текущие размеры окна аплета
    dimAppWndDimension = size();
    
    // Выбираем в контекст отображения желтый цвет
    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);
  }

  // -------------------------------------------------------
  // start
  // Метод вызывается при первом отображении окна аплета
  // -------------------------------------------------------
  public void start()
  {
    if (m_Scroller == null)
    {
      m_Scroller = new Thread(this);
      m_Scroller.start();
    }
  }
	
  // -------------------------------------------------------
  // stop
  // Метод вызывается, когда окно аплета исчезает с экрана
  // -------------------------------------------------------
  public void stop()
  {
    if (m_Scroller != null)
    {
      m_Scroller.stop();
      m_Scroller = null;
    }
  }

  // -------------------------------------------------------
  // run
  // Метод, который работает в рамках отдельной задачи
  // Он выполняет динамическую свертку строк текста 
  // -------------------------------------------------------
  public void run()
  {
    // Счетчик сдвинутых строк
    int ShiftsCounter = 0;

    // Размер сдвига по вертикали
    int yShift;
    
    // Высота символов текста
    int yChar;

    // Номер текущей рисуемой строки
    int CurrentStr = 0;
    
    // Массив сдвигаемых строк
    String s[] = new String[6];

    // Инициализация массива строк
    s[0] = m_String1;
    s[1] = m_String2;
    s[2] = m_String3;
    s[3] = m_String4;
    s[4] = m_String5;
    s[5] = m_String6;

    // Получаем контекст отображения
    Graphics g = getGraphics();
    Dimension dimAppWndDimension = size();

    // Определяем метрики текущего шрифта
    FontMetrics fm = g.getFontMetrics();

    // Сохраняем полную высоту символов шрифта
    yChar = fm.getHeight();

    // Бесконечный цикл сдвига строк
    while (true)
    {
      try
      {
        // Увеличиваем содержимое счетчика сдвигов        
        ShiftsCounter++;

        // Если сдвинута полная строка, рисуем
        // следующую строку в нижней части окна
        if(ShiftsCounter == yChar + 5)
        {
          // Сбрасываем счетчик сдвигов
          ShiftsCounter = 0;
          
          // Первую строку отображаем красным цветом,
          // остальные - черным
          if(CurrentStr == 0)
            g.setColor(Color.red);
          else
            g.setColor(Color.black);

          // Рисуем строку
          g.drawString(s[CurrentStr], 
            10, dimAppWndDimension.height - 10);
          
          // Увеличиваем счетчик строк
          CurrentStr++;

          // Если уже нарисовали шесть строк, сбрасываем
          // счетчик строк
          if(CurrentStr > 5)
            CurrentStr = 0;
        }

        // Устанавливаем шаг сдвига равным одному пикселу
        yShift = 1;

        // Выполняем свертку 
        g.copyArea(0, yShift + 1, 
          dimAppWndDimension.width  - 1, 
          dimAppWndDimension.height - 1,
          0, -yShift);

        // Закрашиваем область ввода желтым цветом
        g.setColor(Color.yellow);

        g.fillRect(1, dimAppWndDimension.height 
          - yShift - 1, 
          dimAppWndDimension.width  - 2, 
          dimAppWndDimension.height - 1);
        
        // Выполняем задержку в 50 миллисекунд
        Thread.sleep(50);
      }
      catch (InterruptedException e)
      {
        stop();
      }
    }
  }
}

В листинге 1.8 вы найдете исходный текст документа HTML, который был создан для аплета Scroller.

Листинг 1.8. Файл Scroller\Scroller.html

<html>
<head>
<title>Scroller</title>
</head>
<body>
<hr>
<applet
    code=Scroller.class
    id=Scroller
    width=400
    height=240 >
    <param name=String1 value="Мы представляем наши новые книги:">
    <param name=String2 value="Том 29. Сервер Web своими руками">
    <param name=String3 value="Том 30. Microsoft Visual J++. Создание приложений на языке Java. Часть 1">
    <param name=String4 value="Том 31. Разработка приложений для Internet с Visual C++ и MFC">
    <param name=String5 value="Том 32. Microsoft Visual J++. Создание приложений на языке Java. Часть 2">
    <param name=String6 value="">
</applet>
<hr>
<a href="Scroller.java">The source.</a>
</body>
</html>

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

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

Основной класс аплета реализует интерфейс Runnable, поэтому для задачи рисования и сдвига строк текста мы не создаем свой класс на базе класса Thread.

Поля класса Scroller

В поле m_Scroller записывается ссылка на задачу рисования и сдвига строк текста.

Шесть полей типа String с именами от m_String1 до m_String6 предназначены для хранения сдвигаемых строк текста.

Поля с именами от PARAM_String1 до PARAM_String6 хранят имена параметров аплета.

Метод init

Единственная задача метода init нашего приложения - получение параметров аплета и запись их в соотвестствующие поля класса. Эта задача решается с помощью метода getParameter, при этом строка param типа String используется как рабочая:

String param;
param = getParameter(PARAM_String1);
if (param != null)
  m_String1 = param;

Аналогичным образом метод получает значения всех шести параметров.

Метод paint

Метод paint подготавливает окно аплета для рисования - закрашивает его в желтый цвет и рисует вокруг окна черную рамку.

Метод start

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

Метод stop

Метод stop основного класса останавливает работу задачи, когда пользователь покидает страницу сервера Web с аплетом, вызывая для этого метод stop.

Метод run

Внутри метода run мы определили массив строк, проинициализировав его значениями, полученными из параметров аплета:

String s[] = new String[6];
s[0] = m_String1;
s[1] = m_String2;
s[2] = m_String3;
s[3] = m_String4;
s[4] = m_String5;
s[5] = m_String6;

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

Так как для рисования строк текста нужно знать контекст отображения, мы получаем его при помощи метода getGraphics:

Graphics g = getGraphics();

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

Dimension dimAppWndDimension = size();

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

FontMetrics fm = g.getFontMetrics();
yChar = fm.getHeight();

В рамках бесконечного цикла мы подсчитываем количество сдвигов (в счетчике ShiftsCounter), а также сдвинутые строки (в счетчике CurrentStr). Заметим, что для обеспечения плавности сдвига мы перемещаем строки по одному пикселу. Когда величина сдвига достигает высоты символов yChar плюс 5, метод run рисует новую строку.

Перед рисованием строки мы выбираем в контекст отображения красный или черный цвет, в зависимости от номера строки:

if(CurrentStr == 0)
  g.setColor(Color.red);
else
  g.setColor(Color.black);

Вы можете выделять нужные вам строки любым другим способом, например, наклоном или жирным шрифтом.

Для рисования строки мы вызываем метод drawString:

g.drawString(s[CurrentStr], 
  10, dimAppWndDimension.height - 10);

Строка будет нарисована на десять пикселов выше нижней границы окна аплета.

После рисования строки мы проверяем, последняя она, или нет:

CurrentStr++;
if(CurrentStr > 5)
  CurrentStr = 0;

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

Для выполнения свертки мы вызываем метод copyArea, знакомый вам по 30 тому “Библиотеки системного программиста”:

yShift = 1;
g.copyArea(0, yShift + 1, 
  dimAppWndDimension.width  - 1, 
  dimAppWndDimension.height - 1,
  0, -yShift);

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

После сдвига освободившаяся область закрашивается желтым цветом:

g.setColor(Color.yellow);
g.fillRect(1, dimAppWndDimension.height - yShift - 1, 
  dimAppWndDimension.width  - 2, 
  dimAppWndDimension.height - 1);

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

Thread.sleep(50);

Приложение HorzScroll

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

Реализация эффекта бегущей его достаточно проста. Аплет создает задачу, которая периодически перерисовывает окно, вызывая метод repaint. Метод paint отображает строку в окне, каждый раз изменяя ее начальные координаты для получения эффекта сдвига.

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

Рис. 1.5. Окно аплета HorzScroll

В чем сложность создания такого аплета?

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

Исходные тексты приложения

Исходные тексты приложения HorzScroll приведены в листинге 1.9.

Листинг 1.9. Файл HorzScroll\HorzScroll.java

// =========================================================
// Рисование строки текста по буквам
//
// (C) Фролов А.В, 1997
//
// E-mail: frolov@glas.apc.org
// WWW:    http://www.glasnet.ru/~frolov
//            или
//         http://www.dials.ccas.ru/frolov
// =========================================================
import java.applet.*;
import java.awt.*;

public class HorzScroll extends Applet implements Runnable
{
  // Ссылка на задачу рисования строки текста
  Thread m_HorzScroll = null;

  // Значения параметров по умолчанию
  private String m_Str   = "Hello from Java!";
  private String m_Fnt   = "Helvetica";
  private String m_style = "BOLD";
  private int m_size     = 12;
  private String m_color = "red";
  private int m_delay    = 50;

  // Имена параметров
  private final String PARAM_str   = "str";
  private final String PARAM_font  = "font";
  private final String PARAM_style = "style";
  private final String PARAM_size  = "size";
  private final String PARAM_color = "color";
  private final String PARAM_delay = "delay";

  // -------------------------------------------------------
  // getAppletInfo
  // Метод, возвращающей строку информации об аплете
  // -------------------------------------------------------
  public String getAppletInfo()
  {
    return "Name: HorzScroll\r\n" +
      "Author: Alexandr Frolov\r\n" +
      "E-mail: frolov@glas.apc.org" +
      "WWW:    http://www.glasnet.ru/~frolov" +
      "Created with Microsoft Visual J++ Version 1.0";
  }

  // -------------------------------------------------------
  // getParameterInfo
  // Иинформация о параметрах
  // -------------------------------------------------------
  public String[][] getParameterInfo()
  {
    String[][] info =
    {
      { PARAM_str,   "String", "String to draw" },
      { PARAM_font,  "String", "Font name" },
      { PARAM_style, "String", "Font style" },
      { PARAM_size,  "String", "Font size" },
      { PARAM_color, "String", "String color" },
      { PARAM_delay, "int",    "Output delay" },
    };
    return info;    
  }

  // -------------------------------------------------------
  // init
  // Метод, получающий управление при инициализации аплета
  // -------------------------------------------------------
  public void init()
  {
    // Рабочая строка
    String param;

    // Получение значений параметров
    param = getParameter(PARAM_str);
    if (param != null)
      m_Str = param;

    param = getParameter(PARAM_font);
    if (param != null)
      m_Fnt = param;

    param = getParameter(PARAM_style);
    if (param != null)
      m_style = param;

    param = getParameter(PARAM_size);
    if (param != null)
      m_size = Integer.parseInt(param);

    param = getParameter(PARAM_color);
    if (param != null)
      m_color = param;

    param = getParameter(PARAM_delay);
    if (param != null)
      m_delay = Integer.parseInt(param);
  }

  // -------------------------------------------------------
  // paint
  // Метод paint, выполняющий рисование в окне аплета
  // -------------------------------------------------------
  public void paint(Graphics g)
  {
    // Определяем текущие размеры окна аплета
    Dimension dimAppWndDimension = size();

    // Определяем текущие размеры окна аплета
    dimAppWndDimension = size();
    
    // Выбираем в контекст отображения желтый цвет
    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);
  }

  // -------------------------------------------------------
  // start
  // Метод вызывается при первом отображении окна аплета
  // -------------------------------------------------------
  public void start()
  {
    if (m_HorzScroll == null)
    {
      m_HorzScroll = new Thread(this);
      m_HorzScroll.start();
    }
  }
  
  // -------------------------------------------------------
  // stop
  // Метод вызывается, когда окно аплета исчезает с экрана
  // -------------------------------------------------------
  public void stop()
  {
    if (m_HorzScroll != null)
    {
      m_HorzScroll.stop();
      m_HorzScroll = null;
    }
  }

  // -------------------------------------------------------
  // run
  // Метод, который работает в рамках отдельной задачи
  // Он выполняет рисование текстовой строки по буквам
  // -------------------------------------------------------
  public void run()
  {
    // Получаем контекст отображения
    Graphics g = getGraphics();

    // Выбираем шрифт в контекст отображения
    if(m_style.equals("BOLD"))
      g.setFont(new Font(m_Fnt, Font.BOLD, m_size));

    else if(m_style.equals("ITALIC"))
      g.setFont(new Font(m_Fnt, Font.ITALIC, m_size));

    else 
      g.setFont(new Font(m_Fnt, Font.PLAIN, m_size));

    // Выбираем цвет в контекст отображения
    if(m_color.equals("red"))
      g.setColor(Color.red);

    else if(m_color.equals("green"))
      g.setColor(Color.green);

    else 
      g.setColor(Color.black);
    
    // Определяем размеры окна аплета
    Dimension dimAppWndDimension = size();

    // Определяем метрики текущего шрифта
    FontMetrics fm = g.getFontMetrics();

    // Номер текущего рисуемого символа строки
    int nCurrentChar = 0;

    // Позиция для рисования по вертикали
    int yPos = fm.getHeight() + 5;

    // Текущая позиция рисования символа по горизонтали
    int nCurrentXPos = 10;

    // Ширина текущего символа в пикселах
    int nCurrentCharWidth;
    
    // Бесконечный цикл рисования
    while (true)
    {
      try
      {
        try
        {
          // Определяем ширину текущего символа
          nCurrentCharWidth = 
            fm.charWidth(m_Str.charAt(nCurrentChar));

          // Массив для преобразования кода символа в сивол
          char[] ch;
    
          // Временная строка
          String s;

          // Создаем массив из одного элемента
          ch = new char[1];

          // Записыаем в него код нажатой клавиши
          ch[0] = m_Str.charAt(nCurrentChar);
    
          // Преобразуем в строку
          s = new String(ch);

          // Рисуем текущий символ в текущей позиции
          g.drawString(s, nCurrentXPos, yPos);

          // Увеличиваем текущую позицию на ширину
          // нарисованного символа
          nCurrentXPos += nCurrentCharWidth;

          // Переходим к следующему символу в строке
          nCurrentChar++;
        }
        // Обработка выхода за пределы строки
        catch (StringIndexOutOfBoundsException e)
        {
          // Сбрасываем номер текущего символа и
          // текущую позицию
          nCurrentChar = 0;
          nCurrentXPos = 10;

          // Перерисовываем окно аплета
          repaint();

          // Задержка после перерисовки окна
          try
          {
            Thread.sleep(500);
          }
          catch (InterruptedException ee)
          {
            stop();
          }
        }

        // Задержка между рисованием отдельных символов        
        Thread.sleep(m_delay);
      }
      catch (InterruptedException e)
      {
        stop();
      }
    }
  }
}

В листинге 1.10 приведен исходный текст документа HTML, созданного для аплета HorzScroll.

Листинг 1.10. Файл HorzScroll\HorzScroll.html

<html>
<head>
<title>HorzScroll</title>
</head>
<body>
<hr>
<applet
    code=HorzScroll.class
    id=HorzScroll
    width=170
    height=50 >
    <param name=str value="Hello from Java!">
    <param name=font value="Helvetica">
    <param name=style value="BOLD">
    <param name=size value="18">
    <param name=color value="red">
    <param name=delay value=100>
</applet>
<hr>
<a href="HorzScroll.java">The source.</a>
</body>
</html>

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

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

Поля класса HorzScroll

В поле m_HorzScroll хранится сслыка на задачу рсиования строки.

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

В поле m_Str хранится отображаемая строка.

Шритф, стилевое оформление и размер смиволов строки хранится, соответственно, в полях m_Fnt, m_style и m_size. Цвет символов строки одинаковый и хранится в поле m_color.

Поле m_delay хранит текущее время задержки между отображением отдельных символов строки в миллисекундах.

Метод init

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

Метод paint

Метод paint просто закрашивает окно аплета желтым цветом и затем обводит его черной рамкой.

Метод run

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

Прежде всего, метод run получает контекст отображения:

Graphics g = getGraphics();

Затем в этом контексте отображения устанавливается шрифт с жирным, наклонным или обычным начертанием:

if(m_style.equals("BOLD"))
  g.setFont(new Font(m_Fnt, Font.BOLD, m_size));
else if(m_style.equals("ITALIC"))
  g.setFont(new Font(m_Fnt, Font.ITALIC, m_size));
else 
  g.setFont(new Font(m_Fnt, Font.PLAIN, m_size));

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

В зависимости от содержимого поля m_color метод run устанавливает один из трех цветов для отображения символов текстовой строки:

if(m_color.equals("red"))
  g.setColor(Color.red);
else if(m_color.equals("green"))
  g.setColor(Color.green);
else 
  g.setColor(Color.black);

Помимо этого, до запуска цикла метод run получает размеры окна аплета и метрики шрифта, только что установленного в контексте отображения:

Dimension dimAppWndDimension = size();
FontMetrics fm = g.getFontMetrics();

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

Кроме того, вычисляется позиция для рисования строки по вертикальной оси yPos и устанавливается начальная позиция первого символа строки по горизонтальной оси nCurrentXPos:

int yPos = fm.getHeight() + 5;
int nCurrentXPos = 10;

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

Первое, что метод run делает в этом цикле, это вычисление ширины текущего символа, сохраняя ее в переменной nCurrentCharWidth:

nCurrentCharWidth = 
  fm.charWidth(m_Str.charAt(nCurrentChar));

Текущий символ извлекается из строки при помощи метода charAt, определенном в классе String. Ширина извлеченного таким образом символа символа определяется методом charWidth из класса метрик шрифта FontMetrics.

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

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

Эта задача решается достаточно просто.

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

char[] ch;
String s;
ch = new char[1];

В самый первый элемент массива мы записываем текущий символ:

ch[0] = m_Str.charAt(nCurrentChar);

Затем мы создаем строку из массива, пользуясь конструктором, специально предусмотренным для этой цели в классе String:

s = new String(ch);

Все! Теперь можно отображать строку, состоящую из одного символа, в текущей позиции:

g.drawString(s, nCurrentXPos, yPos);

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

nCurrentXPos += nCurrentCharWidth;
nCurrentChar++;

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

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

Синхронизация задач

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

public synchronized void decrement()
{
  . . .
}

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

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

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

. . .
synchronized(Account)
{
  if(Account.check(3000000))
     Account.decrement(3000000);
}
. . .

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

Блокировка задачи

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

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

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

try
{
  Thread.sleep(500);
}
catch (InterruptedException ee)
{
  . . .
}

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

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

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

Методы suspend и resume позволяют, соответственно, временно приостанавливать и возобновлять работу задачи. Мы уже пользовались этими методами в приложении 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
    {
      Thread.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 наносекунд. Учтите, что реально вы не сможете указывать время с точностью до наносекунд, так как дискретность системного таймера компьютера намного больше.

Приложение Synchro

Для иллюстрации способа синхронизации задач с помощью методов wait и notify мы подготовили приложение Synchro. Внешне окно аплета этого приложения выглядит точно так же, как и окно аплета Rectangles (рис. 1.2).

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

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

Исходные тексты приложения

Исходные тексты приложения Synchro приведены в листинге 1.11.

Листинг 1.11. Файл Synchro\Synchro.javal

// =========================================================
// Демонстрация синхронизации двух задач
//
// (C) Фролов А.В, 1997
//
// E-mail: frolov@glas.apc.org
// WWW:    http://www.glasnet.ru/~frolov
//            или
//         http://www.dials.ccas.ru/frolov
// =========================================================
import java.applet.*;
import java.awt.*;

// =========================================================
// Основной класс аплета
// =========================================================
public class Synchro extends Applet
{
  // Ссылка на задачу рисования прямоугольников
  DrawRectangles m_DrawRectThread = null;

  // Ссылка на задачу, периодически разблокирующую задачу
  // рисования прямоугольников
  NotifyTask m_NotifyTaskThread = null;

  // -------------------------------------------------------
  // getAppletInfo
  // Метод, возвращающей строку информации об аплете
  // -------------------------------------------------------
  public String getAppletInfo()
  {
    return "Name: Synchro\r\n" +
      "Author: Alexandr Frolov\r\n" +
      "E-mail: frolov@glas.apc.org" +
      "WWW:    http://www.glasnet.ru/~frolov" +
      "Created with Microsoft Visual J++ Version 1.0";
  }

  // -------------------------------------------------------
  // paint
  // Метод paint, выполняющий рисование в окне аплета
  // -------------------------------------------------------
  public void paint(Graphics g)
  {
    // Определяем текущие размеры окна аплета
    Dimension dimAppWndDimension = size();
    
    // Выбираем в контекст отображения желтый цвет
    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);
  }

  // -------------------------------------------------------
  // start
  // Метод вызывается при первом отображении окна аплета
  // -------------------------------------------------------
  public void start()
  {
    if (m_DrawRectThread == null)
    {
      m_DrawRectThread = new DrawRectangles(this);
      m_DrawRectThread.start();
    }

    if (m_NotifyTaskThread == null)
    {
      // Создаем задачу, передавая ей ссылку на
      // задачу рисования прямоугольников, которую
      // необходимо периодически разблокировать
      m_NotifyTaskThread = new NotifyTask(m_DrawRectThread);
      m_NotifyTaskThread.start();
    }
  }
	
  // -------------------------------------------------------
  // stop
  // Метод вызывается, когда окно аплета исчезает с экрана
  // -------------------------------------------------------
  public void stop()
  {
    if (m_DrawRectThread != null)
    {
      m_DrawRectThread.stop();
      m_DrawRectThread = null;
    }

    if (m_NotifyTaskThread != null)
    {
      m_NotifyTaskThread.stop();
      m_NotifyTaskThread = null;
    }
  }
}

// =========================================================
// Класс задачи для рисования прямоугольников
// =========================================================
class DrawRectangles extends Thread
{
  // Контекст отображения окна аплета
  Graphics g;

  // Размеры окна аплета
  Dimension dimAppWndDimension;

  // -------------------------------------------------------
  // DrawRectangles
  // Конструктор класса DrawRectangles
  // -------------------------------------------------------
  public DrawRectangles(Applet Appl)
  {
    // Получаем и сохраняем контекст отображения
    g = Appl.getGraphics();

    // Определяем текущие размеры окна аплета
    dimAppWndDimension = Appl.size();
  }

  // -------------------------------------------------------
  // run
  // Метод, который работает в рамках отдельной задачи
  // Он рисует в окне аплета прямоугольники случайного
  // цвета, размера и расположения
  //
  // Этот метод должен быть определен как synchronized
  // -------------------------------------------------------
  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.fillRect(x, y, width, height);

      // Переводим задачу в сотояние ожидания, в котором
      // она будет находиться до тех пор, пока не будет
      // разблокирована задачей класса NotifyTask
      try
      {
        Thread.wait();
      }
      catch (InterruptedException e)
      {
      }
    }
  }
}

// =========================================================
// Класс задачи для периодического разблокирования
// задачи рисования прямоугольников
// =========================================================
class NotifyTask extends Thread
{
  // Ссылка на задачу, которую необходимо разблокировать
  Thread STask;

  // -------------------------------------------------------
  // Конструктор класса NotifyTask
  // -------------------------------------------------------
  public NotifyTask(Thread SynchroTask)
  {
    // Сохраняем ссылку на задачу, которую необходимо
    // разблокировать
    STask = SynchroTask;
  }

  // -------------------------------------------------------
  // run
  // Метод, который работает в рамках отдельной задачи и
  // периодически разблокирует задачу STask
  // -------------------------------------------------------
  public void run()
  {
    while (true)
    {
      // Выполняем задержку на 30 миллисекунд
      try
      {
        Thread.sleep(30);
      }
      catch (InterruptedException e)
      {
      }

      // Получаем объект STask в монопольное владение
      // и вызываем для него метод notify, 
      // разблокируя работу соотвестсвующей задачи
      synchronized(STask)
      {
        STask.notify();
      }
    }
  }
}

Файл документа HTML, созданный автоматически для нашего аплета, вы найдете в листинге 1.12.

Листинг 1.12. Файл Synchro\Synchro.html

<html>
<head>
<title>Synchro</title>
</head>
<body>
<hr>
<applet
    code=Synchro.class
    id=Synchro
    width=320
    height=240 >
</applet>
<hr>
<a href="Synchro.java">The source.</a>
</body>
</html>

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

Рассмотрим поля и самые важные методы, определенные в классах приложения Synchro.

Поля основного класса аплета

В основном классе аплета определены две ссылки m_DrawRectThread и m_NotifyTaskThread:

DrawRectangles m_DrawRectThread = null;
NotifyTask m_NotifyTaskThread = null;

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

Метод start основного класса

Метод start создает и запускает на выполнение две задачи. Первая задача создается как объект класса DrawRectangles, вторая - как объект класса NotifyTask:

if (m_DrawRectThread == null)
{
  m_DrawRectThread = new DrawRectangles(this);
  m_DrawRectThread.start();
}
if (m_NotifyTaskThread == null)
{
  m_NotifyTaskThread = new NotifyTask(m_DrawRectThread);
  m_NotifyTaskThread.start();
}

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

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

Метод stop основного класса

Когда пользователь покидает страницу с аплетом, метод stop останавливает работу обеих задач, вызывая для них метод stop из класса Thread:

if (m_DrawRectThread != null)
{
  m_DrawRectThread.stop();
  m_DrawRectThread = null;
}
if (m_NotifyTaskThread != null)
{
  m_NotifyTaskThread.stop();
  m_NotifyTaskThread = null;
}

Поля класса DrawRectangles

В поле g класса Graphics хранится контекст отображения окна аплета, определенный конструктором класса DrawRectangles.

Поле dimAppWndDimension хранит размеры окна аплета.

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

Для получения контекста отображения окна аплета, ссылка на который передается через единственный параметр, конструктор класса DrawRectangles вызывает метод getGraphics:

public DrawRectangles(Applet Appl)
{
  g = Appl.getGraphics();
  dimAppWndDimension = Appl.size();
}

Размеры окна аплета определяются с помощью метода size.

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

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

public synchronized void run()
{
  . . .
}

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

try
{
  Thread.wait();
}
catch (InterruptedException e)
{
}

Поля класса NotifyTask

В классе NotifyTask мы определили одно поле STask класса Thread, которое хранит ссылку на задачу, работой которой управляет данный класс. Конструктор класса NotifyTask записывает сюда ссылку на задачу рисования прямоугольников.

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

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

synchronized(STask)
{
  STask.notify();
}

Задачи-демоны

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

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

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