Электронная библиотека книг Александра Фролова и Григория Фролова.
Shop2You.ru Создайте свой интернет-магазин
Библиотека
Братьев
Фроловых
[Назад]

Немного Java - и страница ожила. Часть 1

А.В. Фролов, Г.В. Фролов,  (МИР ПК #12/97)
E-mail: frolov@glasnet.ru 

Исходные тексты программ

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

Разновидностью приложений для WWW являются так называемые аплеты, они довольно часто применяются при оформлении Web-страниц. Аплеты оживляют страницы, добавляя в них анимацию или удобный интерфейс пользователя. Это намного улучшает внешний вид страниц и, как следствие, привлекает внимание посетителей сервера.

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

Те из вас, кто создавал программы для DOS или Windows, знают, что любой процесс можно периодически запускать в ответ на прерывания системного таймера. Примером тому может служить перерисовка окна при анимации изображения. DOS-программа определяет для этого обработчик прерывания таймера INT 8, а приложение Windows обрабатывает сообщение WM_TIMER. К сожалению, в аплете Java вы не сможете вызывать с помощью системного таймера метод paint, внутри которого производится перерисовка окна аплета. Причина проста: в языке Java для этого нет соответствующих средств. Однако можно запустить дополнительный поток, работающий одновременно с главным потоком аплета. Он будет перерисовывать изображение с заданным интервалом времени. Именно этот прием применяется в большинстве анимационных аплетов.

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

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

Интерфейс Runnable

Самый простой способ сделать аплет Java многопоточным - реализовать в нем интерфейс Runnable, как это показано ниже:

public class MultiThreadApp extends Applet 
 implements Runnable
{
 private Thread m_MyThread = null;
 . . .
 public void start()
 {
  if (m_MyThread == null)
  {
   m_MyThread = new Thread(this);
   m_MyThread.start();
  }
 }
 public void run()
 {
  . . .
 }
}

Здесь класс MultiThreadApp наследует класс Applet (так как мы создаем аплет) и дополнительно реализует интерфейс Runnable. Так как в Java нет множественного наследования, мы не можем создать класс на базе классов Applet и Thread, однако можно реализовать произвольное количество интерфейсов, чем мы и воспользовались.

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

Ниже приведены прототипы конструкторов, с помощью которых можно создавать новые потоки:

  • создание нового объекта Thread
  • public Thread();
  • создание нового потока с указанием объекта, для которого будет вызван метод run
  • public Thread(Runnable target);
  • аналогично предыдущему, но дополнительно задается имя нового потока
  • public Thread(Runnable target, String name);
  • создание потока с указанием его имени
  • public Thread(String name);
  • создание нового потока с указанием группы и объекта, для которого вызывается метод run
  • public Thread(ThreadGroup group, Runnable target);
  • аналогично предыдущему, но дополнительно задается имя нового потока
  • public Thread(ThreadGroup group, Runnable target, String name);
  • создание нового потока с указанием группы и ее имени
  • public Thread(ThreadGroup group, String name).

    Чаще всего применяется второй конструктор из перечисленных в этом списке, с одним параметром-ссылкой на объект, для которого будет вызываться метод run. При использовании интерфейса Runnable метод run определен в главном классе приложения, поэтому в качестве параметра конструктору передается значение ссылки на этот класс (this). Обратите внимание, что в классе MultiThreadApp мы определили метод run. Его код будет работать как отдельный поток одновременно с кодом других методов аплета. Заметим, что метод run не вызывается напрямую, а получает управление при запуске потока методом start интерфейса Runnable.

    Из нашего класса MultiThreadApp помимо метода start можно вызывать и другие многочисленные методы интерфейса Runnable, предназначенные для управления потоками.

    Создание потоков как объектов класса Thread

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

    Каждый такой класс соответствует одному потоку и имеет свой собственный метод run.

    В классе аплета вам нужно определить необходимое количество объектов класса DrawPoints, при этом интерфейс Runnable реализовывать не требуется (см. листинг 2).

    Для создания потока, как и любого другого объекта в Java, применяется оператор new.

    Управление потоками

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

    Запуск потока

    Для запуска потока на выполнение вы должны вызвать метод start:

    public void start();

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

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

    public final boolean isAlive();

    Остановка потока

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

    public final void stop();

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

    public final void stop(Throwable obj);

    И наконец, можно принудительно остановить и уничтожить поток методом destroy:

    public void destroy();

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

    public void interrupt();

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

    public static void sleep(long ms);
    public static void sleep(long ms, int ns);

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

    С помощью одного из двух вариантов метода sleep вы можете задержать выполнение потока на заданное время:

    public final void join(long ms);
    public final void join(long ms, int ns);

    В первом варианте время задержки задается в миллисекундах, а во втором - в миллисекундах и наносекундах. Учтите, однако, что системный таймер компьютера вырабатывает прерывания примерно 18,2 раза в секунду, что значительно снижает точность выполнения задержки.

    Метод suspend временно приостанавливает работу потока:

    public final void suspend();

    Для продолжения работы потока необходимо вызвать метод resume:

    public final void resume();

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

    В многопоточной среде часто приходится синхронизировать одновременно работающие потоки. Для этого можно применять методы suspend и resume. А можно использовать метод join, который дожидается завершения потока:

    public final void join();

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

    public final void join(long ms);
    public final void join(long ms, int ns);

    Установка приоритетов потоков

    Метод setPriority, определенный в классе Thread, изменяет приоритет потока:

    public final void setPriority(int newPriority);

    В качестве параметра этому методу передается одно из трех значений:

  • NORM_PRIORITY - нормальный;
  • MAX_PRIORITY - максимальный;
  • MIN_PRIORITY - минимальный.
  • По умолчанию запущенный поток имеет нормальный приоритет и на равных правах конкурирует с потоком, в рамках которого выполняется запустившее эту задачу приложение. Однако при необходимости вы можете увеличить или уменьшить внедрение приоритета, указав методу setPriority значения MAX_PRIORITY и MIN_PRIORITY соответственно. Конечно, эта примитивная система управления приоритетами в Java намного проще, чем, к примеру, аналогичная в Microsoft Windows NT. Однако не забывайте, что приложения Java рассчитаны на работу в среде разных операционных систем, где более мощные средства управления приоритетами могут быть и не предусмотрены. Для потоков, выполняющих фоновую работу, можно установить минимальный приоритет. Что же касается потоков, взаимодействующих с пользователем, то для них лучше оставить нормальный или установить максимальный приоритет. В этом случае приложение будет быстрее реагировать на действия пользователя. С помощью метода getPriority вы можете определить текущий приоритет:

    public final int getPriority();

    Этот метод возвращает одно из трех значений приоритета, перечисленных выше.

    Некоторые методы класса Thread

  • Остановка текущего потока для того, чтобы управление было передано другому потоку
  • public static void yield();
  • Вызвав метод yield, поток может отдать системе не израсходованный им остаток своего кванта времени выполнения.
  • Определение текущего работающего потока
  • public static Thread currentThread();
  • Метод currentThread возвращает ссылку на поток, который работает в текущий момент времени.
  • Установка имени потока
  • public final void setName(String name);

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

  • Определение имени потока
  • public final String getName();
  • Определение группы, к которой принадлежит поток
  • public final ThreadGroup getThreadGroup();

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

  • Получение всех потоков, входящих в данную группу
  • public static int enumerate(Thread tarray[]);

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

  • Установка режима демона
  • public final void setDaemon(boolean on);

    Для любого потока Java вы можете установить так называемый режим демона. Потоки-демоны служат в качестве сервисных для других потоков, т. е. они принимают запросы и выполняют их в своем методе run. Для того чтобы поток стал демоном, вы должны вызвать метод setDaemon с параметром true.

  • Определение, является ли поток демоном
  • public final boolean isDaemon();

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

    Аплет SimpleScroll

    В нашем первом многопотоковом аплете мы изобразим в окне аплета бегущую текстовую строку. Такую строку вы можете увидеть на страницах многих серверов Web.

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

    Как достигается эффект бегущей строки? Аплет SimpleScroll запускает поток, который в цикле перерисовывает окно аплета, вызывая для этого метод repaint. После каждого вызова метода repaint выполняется задержка на 50 мс.

    Как известно, при перерисовке окна аплета вызывается метод paint. В нашем аплете он рисует текстовую строку, причем каждый раз сдвигает ее начало влево на один пиксел. В paint, как вы, наверное, уже заметили, нет никакого цикла, внутри которого выполняется рисование, однако сам метод paint вызывается периодически методом Repaint из второго потока. Полный исходный текст аплета SimpleScroll приведен в листинге 3.

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

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

    Многие RAD-продукты для создания Java-приложений имеют в своем составе различные опции автоматизации разработки, и система Microsoft Visual J++, с помощью которой создавались все примеры, - не исключение. Она может автоматически генерировать заготовку исходного текста Java-приложения, затем ее можно дополнить в соответствии с задачами, решаемыми приложением. В соответствующей диалоговой панели можно указать, что необходимо подготовить исходные тексты многопоточного аплета.

    Созданный таким образом аплет использует способ реализации многопоточности, основанный на применении интерфейса Runnable:

    public class SimpleScroll 
     extends Applet implements Runnable
    {
     private Thread m_SimpleScroll = null;
     . . .
    }

    В классе SimpleScroll определено поле m_SimpleScroll класса Thread, хранящее ссылку на поток, запускаемый нашим аплетом для периодической перерисовки своего окна. Это поле инициализируется значением null, что необходимо для правильной работы аплета. В поле m_Text типа String хранится текстовая строка, которая передается аплету в документе HTML с помощью специального параметра. Установив дополнительные параметры, можно передавать цвет, стиль и размер шрифта для отображения текстовой строки. Аплет определяет длину строки и записывает ее в поле m_StringSize. Текущая позиция начала отображения строки при вызове метода paint хранится в поле m_CurrentXPosition. Эта позиция периодически уменьшается на один пиксел при каждом вызове метода paint.

    Рассмотрим теперь самые важные методы нашего аплета. Метод init получает управление при инициализации аплета. Прежде всего он получает из документа HTML значение единственного параметра - текстовую строку, которая будет сдвигаться в окне аплета. Параметр извлекается с помощью метода getParameter и сохраняется в поле m_Text. Далее метод init определяет размерности текущего шрифта, выбранного для показа строки, вызывая для этого метод getFontMetrics. В качестве параметра ему передается значение, возвращенное методом getFont. Метрики сохраняются в поле fm. Пользуясь этими метриками, метод init определяет длину строки m_Text с помощью метода stringWidth. После определения длины строки инициализируется поле m_CurrentXPosition, хранящее текущую позицию для рисования сдвигаемой строки. В это поле записывается координата правой границы окна аплета. Ширина окна аплета определяется с помощью метода size. Последнее, что делает метод init, - устанавливает желтый (в нашем случае) цвет для фона окна аплета.

    Следующий метод - paint - рисует периодически сдвигаемую текстовую строку в текущей позиции m_CurrentXPosition:

    g.drawString(m_Text, m_CurrentXPosition, 20);

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

    m_CurrentXPosition-;

    Когда вся строка будет сдвинута влево и исчезнет с экрана, текущая позиция устанавливается равной ширине окна аплета:

    if(m_CurrentXPosition < -m_StringSize)
     m_CurrentXPosition = size().width;

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

    Метод start получает управление при активизации, когда в окне браузера появляется документ HTML с нашим аплетом. Если в поле m_SimpleScroll находится значение null, метод start создает новый поток, который будет периодически перерисовывать окно аплета:

    if (m_SimpleScroll == null)
    {
     m_SimpleScroll = new Thread(this);
     m_SimpleScroll.start();
    }

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

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

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

    Разумеется, остановка выполняется только в том случае, если поток был создан. Это можно узнать по значению поля m_SimpleScroll, в котором должна находиться правильная ссылка на объект. После остановки потока мы записываем в это поле значение null.

    Метод run, как мы уже говорили, не вызывается никаким другим методом, определенным в нашем аплете. Этот метод получает управление и работает в рамках самостоятельного потока, когда для соответствующего объекта вызывается метод start. Это происходит при активизации аплета. Run включает в себя бесконечный цикл, внутри которого вызывается метод repaint, с последующей задержкой на 50 мс:

    try
    {
     repaint();
     Thread.sleep(50);
    }
    catch (InterruptedException e)
    {
     stop();
    }

    При возникновении исключения InterruptedException поток останавливается методом stop.

    Продолжение в следующем номере.


    Александр Вячеславович Фролов, Григорий Вячеславович Фролов - авторы серий книг "Библиотека системного программиста" и "Персональный компьютер. Шаг за шагом".

    E-mail: frolov@glas.apc.org
    Web: http://www.glasnet.ru/~frolov

    Листинг 1

    class DrawPoints extends Thread
    {
     . . . 
     public void run()
     {
      . . .
     }
    }
    class DrawLines extends Thread
    {
     . . . 
     public void run()
     {
      . . .
     }
    }

    Листинг 2

    public class MyApplet extends Applet
    {
     DrawPoints m_DrawPointsThread = null;
     DrawLines m_DrawLinesThread = null;
     . . .
     public void start()
     {
      if (m_DrawPointsThread == null)
      {
       m_DrawPointsThread = new DrawPoints(this);
       m_DrawPointsThread.start();
      }
      if (m_DrawLinesThread == null)
      {
       m_DrawLinesThread = new DrawLines(this);
       m_DrawLinesThread.start();
      }
     }
    }

    Листинг 3

    import java.applet.*;
    import java.awt.*;
    
    public class SimpleScroll 
     extends Applet implements Runnable
    {
     private Thread m_SimpleScroll = null;
     private String m_Text = "Scrolling string";
     private final String PARAM_Text = "Text";
     int m_StringSize;
     int m_CurrentXPosition;
      public String getAppletInfo()
     {
      return "Name: SimpleScroll\r\n" +
       "Author: Alexandr Frolov\r\n" +
       "E-mail: frolov@glas.apc.org\r\n" +
       "Web:  http://www.glasnet.ru/~frolov," + 
       "    http://www.dials.ccas.ru/frolov";
     }
    
     public String[][] getParameterInfo()
     {
      String[][] info =
      {
       { PARAM_Text, "String", "Scrolling string" },
      };
      return info;  
     }
    
     public void init()
     {
      String param;
      param = getParameter(PARAM_Text);
      if (param != null)
       m_Text = param;
      FontMetrics fm = getFontMetrics(getFont());
      m_StringSize = fm.stringWidth(m_Text);
      m_CurrentXPosition = size().width;
      setBackground(Color.yellow);
     }
     public void paint(Graphics g)
     {
      g.drawString(m_Text, m_CurrentXPosition, 20);
      m_CurrentXPosition-;
      if(m_CurrentXPosition < -m_StringSize)
        m_CurrentXPosition = size().width;
     }
    
     public void start()
     {
      if (m_SimpleScroll == null)
      {
       m_SimpleScroll = new Thread(this);
       m_SimpleScroll.start();
      }
     }
     
     public void stop()
     {
      if (m_SimpleScroll != null)
      {
       m_SimpleScroll.stop();
       m_SimpleScroll = null;
      }
     }
    
     public void run()
     {
      while (true)
      {
       try
       {
        repaint();
        Thread.sleep(50);
       }
       catch (InterruptedException e)
       {
        stop();
       }
      }
     }
    }

    Листинг 4

    <html>
    <head>
    <title>SimpleScroll</title>
    </head>
    <body>
    <hr>
    <applet
      code=SimpleScroll.class
      name=SimpleScroll
      width=320
      height=40 >
      <param name=Text value="Scrolling string">
    </applet>
    <hr>
    <a href="SimpleScroll.java">The source.</a>
    </body>
    </html>

    Листинг 5

    import java.applet.*;
    import java.awt.*;
    
    public class ScrNoFlick extends Applet implements Runnable
    {
     private Thread  m_ScrNoFlick = null;
     private String m_Text = "Scrolling String";
     private final String PARAM_Text = "Text";
     int m_StringSize;
     int m_CurrentXPosition;
    
     private Image m_MemImage;
     private Graphics m_MemImage_Graphics;
     Dimension m_MemImageDim = null;
    
     public String getAppletInfo()
     {
      return "Name: ScrNoFlick\r\n" +
       "Author: Alexandr Frolov\r\n" +
       "E-mail: frolov@glas.apc.org\r\n" +
       "Web: http://www.glasnet.ru/~frolov," + 
       " http://www.dials.ccas.ru/frolov";
     }
    
     public String[][] getParameterInfo()
     {
      String[][] info =
      {
       { PARAM_Text, "String", "Scrolling String" },
      };
      return info;  
     }
    
     public void init()
     {
      String param;
    
      param = getParameter(PARAM_Text);
      if (param != null)
       m_Text = param;
    
      FontMetrics fm = getFontMetrics(getFont());
      m_StringSize = fm.stringWidth(m_Text);
      m_CurrentXPosition = size().width;
      setBackground(Color.yellow);
     }
    
     public void update(Graphics g)
     {
      Dimension d = size();
      int nWidth = d.width;
      int nHeight = d.height;
    
      if((m_MemImageDim == null) ||
        (m_MemImageDim.width != nWidth) ||
        (m_MemImageDim.height != nHeight))
      {
       m_MemImageDim = new Dimension(nWidth, nHeight);
       m_MemImage = createImage(nWidth, nHeight);
       m_MemImage_Graphics = m_MemImage.getGraphics();
      }
    
      Color fg = getForeground();
      Color bg = getBackground();
      m_MemImage_Graphics.setColor(bg);
    
      m_MemImage_Graphics.fillRect(0, 0, 
       m_MemImageDim.width, m_MemImageDim.height);
    
      m_MemImage_Graphics.setColor(fg);
    
      m_MemImage_Graphics.drawString(m_Text, m_CurrentXPosition, 20);
      m_CurrentXPosition-;
    
      if(m_CurrentXPosition < -m_StringSize)
        m_CurrentXPosition = size().width;
    
      paint(g);
     }
    
     public void paint(Graphics g)
     {
      if(m_MemImage != null)
       g.drawImage(m_MemImage, 0, 0, null);
     }
    
     public void start()
     {
      if (m_ScrNoFlick == null)
      {
       m_ScrNoFlick = new Thread(this);
       m_ScrNoFlick.start();
      }
     }
     
     public void stop()
     {
      if (m_ScrNoFlick != null)
      {
       m_ScrNoFlick.stop();
       m_ScrNoFlick = null;
      }
     }
     public void run()
     {
      while (true)
      {
       try
       {
        repaint();
        Thread.sleep(50);
       }
       catch (InterruptedException e)
       {
        stop();
       }
      }
     }
    }

     

    Листинг 6

    <html>
    <head>
    <title>ScrNoFlick</title>
    </head>
    <body>
    <hr>
    <applet
      code=ScrNoFlick.class
      name=ScrNoFlick
      width=320
      height=40 >
      <param name=Text value="Scrolling String">
    </applet>
    <hr>
    <a href="ScrNoFlick.java">The source.</a>
    </body>
    </html>

Размещено с разрешения редакции журнала МИР ПК

[Назад]