Думай на Java, 2-я редакция, Проверка 11

╧2000 by Bruce Eckel

[ Предыдущая глава ] [ Оглавление ] [ Содержание ] [ Индекс ] [ Следующая глава ]

14: Множественные нити процессов


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

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

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

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

Отзывчивый пользовательский интерфейс

В качестве отправной точки рассмотрим программу выполняющую какие-либо интенсивные вычисления из-за чего совершенно не реагирует на ввод пользователя. Нижеприведенный код, являющийся апплетом/приложением одновременно, просто выводит показания счетчика:
//: c14:Counter1.java
// A non-responsive user interface.
// <applet code=Counter1 width=300 height=100>
// </applet>
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class Counter1 extends JApplet {
  private int count = 0;
  private JButton
    start = new JButton("Start"),
    onOff = new JButton("Toggle");
  private JTextField t = new JTextField(10);
  private boolean runFlag = true;
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(t);
    start.addActionListener(new StartL());
    cp.add(start);
    onOff.addActionListener(new OnOffL());
    cp.add(onOff);
  }
  public void go() {
    while (true) {
      try {
        Thread.sleep(100);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
      if (runFlag)
        t.setText(Integer.toString(count++));
    }
  }
  class StartL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      go();
    }
  }
  class OnOffL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      runFlag = !runFlag;
    }
  }
  public static void main(String[] args) {
    Console.run(new Counter1(), 300, 100);
  }
} ///:~

Swing и апплеты должны быть вам уже знакомы по главе 13. Метод go() это то место программы где выполнение зацикливается: текущее значение count помещается в JTextField t, после чего count увеличивает значение.

Часть бесконечного цикла внутри go() вызов sleep(). Sleep() должен ассоциироваться с объектом Thread, и это должно показывать, что каждое приложение имеет несколько связанных с ним процессов. (Действительно, Java базируется на процессах и всегда есть один, запущенный с вашим приложением.) Таким образом, в зависимости от того где вы точно используете процессы, вы можете вызвать текущий процесс используемый программой при помощи Thread и статического sleep() метода.

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

Когда нажата кнопка Strart выполняется go(). Глянув на код go() вы можете наивно предположить (как и я), что множественность процессов будет соблюдаться, так как процесс засыпает. Таким образом, когда данный метод заснул, CPU должен заниматься опросом других кнопок. На самом деле проблема в том, что go() никогда не завершиться, поскольку цикл бесконечный, а значит actionPerformed( ) не завершиться. Поскольку вы находитесь в actionPerformed( ) после первого нажатия, программа не сможет обработать другие события. (Для выхода необходимо каким-то образом завершить приложение, наиболее простой способ нажать Ctrl+C в консольном окне, если запущено в консоли. Если запущено в броузере, то придется убить броузер.)

Основная проблема заключается в том, что go() должна продолжить выполнение и в то же время завершить выполнение так, чтобы вызов actionPerformed( ) мог завершиться и пользовательский интерфейс мог снова среагировать на действия пользователя. Но обычный метод, похожий на go(), не может продолжить выполнение и вернуть управление основной программе одновременно. Это звучит как неразрешимая проблема, как будто CPU должен находиться сразу в двух местах, но это точно иллюзия создаваемая процессами.

Модель процессов (и ее программирование, поддерживаемое Java) удобное средство программирования для облегчения запуска нескольких операций в одно и то же время в одной программе. С процессами CPU обходит их всех и выделяет каждому квант времени. Каждый процесс считает, что выполняется на CPU единолично, на самом деле время процессора поделено между всеми процессами. Исключением является случай, когда программа запущено на многопроцессорной машине. Но одно важное обстоятельство насчет процессов заключается в том, что вам не нужно думать об этих уровнях, так что коду вашей программы не обязательно знать выполняется он на единственном CPU или на нескольких. Таким образом, процессы дают возможность создавать легко масштабируемые приложения.

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

Наследование от процесса

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

Следующий пример создает произвольное количество процессов, отслеживаемые по присвоенному каждому процессу уникальному номеру, сгенерированному static переменной. Метод run()от Thread переопределен и выполняет уменьшение счетчика при каждом проходе цикла и завершает выполнение когда счетчик равен нулю (в том месте когда run() возвращает значение процесс завершается).

//: c14:SimpleThread.java
// Very simple Threading example.

public class SimpleThread extends Thread {
  private int countDown = 5;
  private static int threadCount = 0;
  private int threadNumber = ++threadCount;
  public SimpleThread() {
    System.out.println("Making " + threadNumber);
  }
  public void run() {
    while(true) {
      System.out.println("Thread " + 
        threadNumber + "(" + countDown + ")");
      if(--countDown == 0) return;
    }
  }
  public static void main(String[] args) {
    for(int i = 0; i < 5; i++)
      new SimpleThread().start();
    System.out.println("All Threads Started");
  }
} ///:~

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

В вызове main( ) содержится несколько созданных и запущенных процессов. Метод start()класса Thread выполняет специальную инициализацию для процесса и затем вызывает run(). Таким образом необходимые действия: вызов конструктора для создания объекта, затем start() конфигурирует процесс и вызов run(). Если не вызвать start() (что делается в конструкторе, если возможно) процесс никогда не будет запущен.

Результат работы программа  для одного из запусков (которые могут отличаться при каждом запуске) следующий:

Making 1
Making 2
Making 3
Making 4
Making 5
Thread 1(5)
Thread 1(4)
Thread 1(3)
Thread 1(2)
Thread 2(5)
Thread 2(4)
Thread 2(3)
Thread 2(2)
Thread 2(1)
Thread 1(1)
All Threads Started
Thread 3(5)
Thread 4(5)
Thread 4(4)
Thread 4(3)
Thread 4(2)
Thread 4(1)
Thread 5(5)
Thread 5(4)
Thread 5(3)
Thread 5(2)
Thread 5(1)
Thread 3(4)
Thread 3(3)
Thread 3(2)
Thread 3(1)

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

Можно также видеть, чтопроцессы  выполняются не в том же порядке в каком они были запущены. Фактически, порядок, в котором CPU обрабатывает существующие процессы, не определен до тех пор, пока не определены приоритеты, используя setPriority() метод класса Thread.

Когда main() создает объекты Thread он не сохраняет ссылки на них. Обычные объекты могут быть просто игрушкой для сборщика мусора, но не Thread. Каждый Thread "регистрирует" себя сам, и таким образом ссылка на него храниться где-то в другом месте из-за чего сборщик мусора не может его очистить.

Использование процессов для пользовательского интерфейса

Вот теперь появилась возможность разрешить проблему из примера Counter1.java с процессами. Решение заключается в правильном размещении подзадачи, т.е. цикла, расположенного внутри go(), который поместим внутрь метода run(). Когда пользователь нажимает кнопку start процесс запускается, но затем создание процесса завершается, и, хотя процесс запущен, основная работа программы, которая заключается в реагировании на действия пользователя, продолжается. Вот решение этой проблемы:
//: c14:Counter2.java
// A responsive user interface with threads.
// <applet code=Counter2 width=300 height=100>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

public class Counter2 extends JApplet {
  private class SeparateSubTask extends Thread {
    private int count = 0;
    private boolean runFlag = true;
    SeparateSubTask() { start(); }
    void invertFlag() { runFlag = !runFlag; }
    public void run() {
      while (true) {
       try {
        sleep(100);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
       if(runFlag) 
         t.setText(Integer.toString(count++));
      }
    }
  } 
  private SeparateSubTask sp = null;
  private JTextField t = new JTextField(10);
  private JButton 
    start = new JButton("Start"),
    onOff = new JButton("Toggle");
  class StartL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(sp == null)
        sp = new SeparateSubTask();
    }
  }
  class OnOffL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(sp != null)
        sp.invertFlag();
    }
  }
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(t);
    start.addActionListener(new StartL());
    cp.add(start);
    onOff.addActionListener(new OnOffL());
    cp.add(onOff);
  }
  public static void main(String[] args) {
    Console.run(new Counter2 (), 300, 100);
  }
} ///:~


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

Класс SeparateSubTask простое расширение от Thread с конструктором, который запускает процесс вызовом start(), а затем run(), который в сущности содержит код от go() из примера Counter1.java.

Из-за того, что SeparateSubTask внутренний класс, он может напрямую обращаться к JTextField t в Counter2; можно видеть как это происходит внутри run(). Поле t во внешнем классе определено как private, поскольку SeparateSubTask может получить к нему доступ без применения специальных разрешений, и всегда желательно делать поле настолько private, насколько это возможно, для того чтобы оно не могло быть случайно изменено извне вашего класса.

Когда нажимаем кнопку onOff она меняет runFlag внутри объекта SeparateSubTask. Данный процесс (когда он проверяет флаг) может самостоятельно остановиться или запуститься. Нажатие кнопки onOff вызывает тут же заметную реакцию. Конечно, в реальности реакция не мгновенная, счетчик остановится только тогда, когда процесс получит свой квант времени от CPU и проверит изменение флага.

Можно видеть, что внутренний  класс SeparateSubTask есть  private, а это значит, что к его полям и методам существует доступ по умолчанию (за исключением run(), который должен быть public поскольку он public в классе предка). Внутренний Private класс недоступен никому, за исключением Counter2 и эти два класса крепко связаны. Всегда, когда вы замечаете классы, которые оказываются крепко связанными друг с другом, рассмотрите возможность оптимизации своего кода и поддержки за счет использования внутренних классов.

Объединение процесса с основным классом

В вышеприведенном примере показан класс процесса отделенной от основного класса программы. Это делает пример более характерным и сравнительно легким для понимания. Существует, однако, альтернативная форма использования, которую вы будете часто видеть и которая не столь проста, но в большинстве случаев более кратка (что вероятно и увеличивает ее популярность). Эта форма объединяет класс основной программы и класс процесса, делая класс основной программы процессом. Поскольку для GUI (графический интерфейс пользователя) программы класс основной программы должен быть наследован как от Frame так и от Applete, наследование может быть использовано для добавления функциональности. Данный интерфейс называется Runnable и содержит те же основные методы что и Thread. Фактически Thread также реализует Runnable, что выражается только в наличии метода run().

Использование совмещенной программы-процесса не столь очевидно. Когда запускается программа, создается объект, который Runnable, но процесс не запускается, что должно быть сделано явно. Это можно пронаблюдать в следующем примере, функционально идентичному Counter2:

//: c14:Counter3.java
// Using the Runnable interface to turn the 
// main class into a thread.
// <applet code=Counter3 width=300 height=100>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

public class Counter3 
    extends JApplet implements Runnable {
  private int count = 0;
  private boolean runFlag = true;
  private Thread selfThread = null;
  private JButton 
    start = new JButton("Start"),
    onOff = new JButton("Toggle");
  private JTextField t = new JTextField(10);
  public void run() {
    while (true) {
      try {
        selfThread.sleep(100);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
      if(runFlag) 
        t.setText(Integer.toString(count++));
    }
  }
  class StartL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(selfThread == null) {
        selfThread = new Thread(Counter3.this);
        selfThread.start();
      }
    }
  }
  class OnOffL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      runFlag = !runFlag;
    }
  }
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(t);
    start.addActionListener(new StartL());
    cp.add(start);
    onOff.addActionListener(new OnOffL());
    cp.add(onOff);
  }
  public static void main(String[] args) {
    Console.run(new Counter3(), 300, 100);
  }
} ///:~
Теперь run() внутри класса, но и после завершения inti() процесс все еще не запущен. Когда вы нажимаете кнопку start, процесс создается (если он еще не существует) следующим непонятным выражением:
new Thread(Counter3.this);
Когда что-либо имеет интерфейс Runnable, это просто означает, что оно имеет метод run( ), однако ничего особеного в этом нет - не производится ни каких задуманных для процесса действий, кроме как наследование класса от Thread. Таким образом, чтобы сделать процесс из Runnable объекта необходимо создать отдельный объект Thread, как показано выше, передав объект Runnable в специальный конструктор Thread. Затем можно вызвать start() для данного процесса:
selfThread.start();

Выполняется обычная инициализация и затем вызов run().

Удобство использования интерфейса Runnable в том, что все принадлежит тому же классу. Если необходимо обращение к чему-либо еще вы просто выполняете это без использования отдельного класса. Однако, как можно было видеть в предыдущем примере, доступ также прост как и использование внутреннего класса [70].

Создание множества процессов

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

Следующий пример повторяет форму примера выше со счетчиками и кнопками переключателями. Но теперь вся информация о каком либо счетчике, включая кнопку и текстовое поле, внутри собственного объекта, который унаследован от Thread. Все поля в Ticker являются private, а это значит, что реализация Ticker может быть изменена по желанию, включая количество и тип компонентов данных для сбора и отображения информации. Когда создается объект Ticker, конструктор добавляет его визуальные компоненты на панель внешнего объекта:

//: c14:Counter4.java
// By keeping your thread as a distinct class,
// you can have as many threads as you want. 
// <applet code=Counter4 width=200 height=600>
// <param name=size value="12"></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

public class Counter4 extends JApplet {
  private JButton start = new JButton("Start");
  private boolean started = false;
  private Ticker[] s;
  private boolean isApplet = true;
  private int size = 12;
  class Ticker extends Thread {
    private JButton b = new JButton("Toggle");
    private JTextField t = new JTextField(10);
    private int count = 0;
    private boolean runFlag = true;
    public Ticker() {
      b.addActionListener(new ToggleL());
      JPanel p = new JPanel();
      p.add(t);
      p.add(b);
      // Calls JApplet.getContentPane().add():
      getContentPane().add(p); 
    }
    class ToggleL implements ActionListener {
      public void actionPerformed(ActionEvent e) {
        runFlag = !runFlag;
      }
    }
    public void run() {
      while (true) {
        if (runFlag)
          t.setText(Integer.toString(count++));
        try {
          sleep(100);
        } catch(InterruptedException e) {
          System.err.println("Interrupted");
        }
      }
    }
  }
  class StartL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(!started) {
        started = true;
        for (int i = 0; i < s.length; i++)
          s[i].start();
      }
    }
  }
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    // Get parameter "size" from Web page:
    if (isApplet) {
      String sz = getParameter("size");
      if(sz != null)
        size = Integer.parseInt(sz);
    }
    s = new Ticker[size];
    for (int i = 0; i < s.length; i++)
      s[i] = new Ticker();
    start.addActionListener(new StartL());
    cp.add(start);
  }
  public static void main(String[] args) {
    Counter4 applet = new Counter4();
    // This isn't an applet, so set the flag and
    // produce the parameter values from args:
    applet.isApplet = false;
    if(args.length != 0)
      applet.size = Integer.parseInt(args[0]);
    Console.run(applet, 200, applet.size * 50);
  }
} ///:~

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

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

<param name=size value="20">

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

Обратите внимание, что определение размера массива s выполняется внутри init() и не является частью определения s. Таким образом, вы не можете сказать какая часть класса определена (вне любого объекта):

int size = Integer.parseInt(getParameter("size"));
Ticker[] s = new Ticker[size];

Можно попытаться скомпилировать данный код, но получите странную ошибку "null-pointer exception" во время выполнения. В то же время все прекрасно работает если переместить инициализацию getParameter() внутрь init( ). Среда выполнения апплетов  выполняет все необходимые действия по перехвату параметров до вызова init().

К тому же данный код является одновременно и  апплетом и  приложением. Когда он выполняется как приложение аргумент size передается как параметр командной строки (или используется значение по умолчанию).

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

Нажатие на кнопку start обозначает цикл по всему массиву Ticker и вызывает start() для каждого.  Запомните, start() выполняет необходимую инициализацию процесса и, затем, вызывает run( ) для каждого процесса.

Слушатель ToggleL просто инвертирует флаг в Ticker и, когда связанный с ним процесс в следующий раз проверит значение, он среагирует соответственно.

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

Можно также поэкспериментировать и убедиться в том, насколько sleep(100)  важен внутри Tricker.run(). Если убрать sleep() все будет прекрасно работать пока вы не нажмете кнопку переключатель, что установит значение runFlag в false после чего run() просто заморозится в бесконечном цикле, который будет трудно прервать во время мульти процессорности, так что время отклика программы и скорость выполнения заметно ухудшаться.

Процессы демоны

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

Можно выяснить, является ли процесс демоном через вызов isDaemon( ), и можно установить или отменить параметры для процесса демона функцией setDaemon( ). Если процесс является демоном, то любой созданный им процесс также является демоном.

Следующий пример демонстрирует создание процесса демона:

//: c14:Daemons.java
// Daemonic behavior.
import java.io.*;

class Daemon extends Thread {
  private static final int SIZE = 10;
  private Thread[] t = new Thread[SIZE];
  public Daemon() { 
    setDaemon(true);
    start();
  }
  public void run() {
    for(int i = 0; i < SIZE; i++)
      t[i] = new DaemonSpawn(i);
    for(int i = 0; i < SIZE; i++)
      System.out.println(
        "t[" + i + "].isDaemon() = " 
        + t[i].isDaemon());
    while(true) 
      yield();
  }
}

class DaemonSpawn extends Thread {
  public DaemonSpawn(int i) {
    System.out.println(
      "DaemonSpawn " + i + " started");
    start();
  }
  public void run() {
    while(true) 
      yield();
  }
}

public class Daemons {
  public static void main(String[] args) 
  throws IOException {
    Thread d = new Daemon();
    System.out.println(
      "d.isDaemon() = " + d.isDaemon());
    // Allow the daemon threads to
    // finish their startup processes:
    System.out.println("Press any key");
    System.in.read();
  }
} ///:~

Процесс Daemon устанавливает соответствующий флаг в значение "true" и затем плодит кучу процессов чтобы показать, что они также демоны. Затем он переходит в бесконечный цикл и вызывает yield() для передачи управления другому приложению. В ранних версиях этой программы бесконечный цикл увеличивал значение счетчика int, но похоже, что это приводило к остановке всей программы. Использование yield() делает программу более устойчивой.

Ничего не удерживает программу от завершения после выполнения основной функции main(), поскольку ничего нет, кроме запущенных процессов демонов. Можно видеть результат работы всех процессов демонов, значение System.in установлено в "чтение", поэтому программа ждет нажатия клавишы. Без этого вы бы увидели только часть результатов от создания процессов демонов. (Попробуйте заменить read() вызовом sleep() с различной продолжительностью, чтобы понаблюдать за выполнением.)

Использование ограниченных ресурсов

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

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

Неправильный доступ к ресурсам

Рассмотрим изменение значения счетчиков, использованных в данной главе. В следующем примере каждый процесс имеет два счетчика, которые увеличивают свои значения и отображаются внутри вызова run(). Дополнительно существует другой процесс класса Watcher, который отслеживает равенство значений показаний счетчиков. Это выглядит как необязательное дополнение, поскольку посмотрев на исходный код можно предположить, что значения счетчиков всегда будут одинаковые. Однако нас ждут сюрпризы. Ниже приведена первая версия программы:
//: c14:Sharing1.java
// Problems with resource sharing while threading.
// <applet code=Sharing1 width=350 height=500>
// <param name=size value="12">
// <param name=watchers value="15">
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

public class Sharing1 extends JApplet {
  private static int accessCount = 0;
  private static JTextField aCount = 
    new JTextField("0", 7);
  public static void incrementAccess() {
    accessCount++;
    aCount.setText(Integer.toString(accessCount));
  }
  private JButton 
    start = new JButton("Start"),
    watcher = new JButton("Watch");
  private boolean isApplet = true;
  private int numCounters = 12;
  private int numWatchers = 15;
  private TwoCounter[] s;
  class TwoCounter extends Thread {
    private boolean started = false;
    private JTextField 
      t1 = new JTextField(5),
      t2 = new JTextField(5);
    private JLabel l = 
      new JLabel("count1 == count2");
    private int count1 = 0, count2 = 0;
    // Add the display components as a panel:
    public TwoCounter() {
      JPanel p = new JPanel();
      p.add(t1);
      p.add(t2);
      p.add(l);
      getContentPane().add(p);
    }
    public void start() {
      if(!started) {
        started = true;
        super.start();
      }
    }
    public void run() {
      while (true) {
        t1.setText(Integer.toString(count1++));
        t2.setText(Integer.toString(count2++));
        try {
          sleep(500);
        } catch(InterruptedException e) {
          System.err.println("Interrupted");
        }
      }
    }
    public void synchTest() {
      Sharing1.incrementAccess();
      if(count1 != count2)
        l.setText("Unsynched");
    }
  }
  class Watcher extends Thread {
    public Watcher() { start(); }
    public void run() {
      while(true) {
        for(int i = 0; i < s.length; i++)
          s[i].synchTest();
        try {
          sleep(500);
        } catch(InterruptedException e) {
          System.err.println("Interrupted");
        }
      }
    }
  }
  class StartL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      for(int i = 0; i < s.length; i++)
        s[i].start();
    }
  }
  class WatcherL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      for(int i = 0; i < numWatchers; i++)
        new Watcher();
    }
  }
  public void init() {
    if(isApplet) {
      String counters = getParameter("size");
      if(counters != null)
        numCounters = Integer.parseInt(counters);
      String watchers = getParameter("watchers");
      if(watchers != null)
        numWatchers = Integer.parseInt(watchers);
    }
    s = new TwoCounter[numCounters];
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    for(int i = 0; i < s.length; i++)
      s[i] = new TwoCounter();
    JPanel p = new JPanel();
    start.addActionListener(new StartL());
    p.add(start);
    watcher.addActionListener(new WatcherL());
    p.add(watcher);
    p.add(new JLabel("Access Count"));
    p.add(aCount);
    cp.add(p);
  }
  public static void main(String[] args) {
    Sharing1 applet = new Sharing1();
    // This isn't an applet, so set the flag and
    // produce the parameter values from args:
    applet.isApplet = false;
    applet.numCounters = 
      (args.length == 0 ? 12 :
        Integer.parseInt(args[0]));
    applet.numWatchers =
      (args.length < 2 ? 15 :
        Integer.parseInt(args[1]));
    Console.run(applet, 350, 
      applet.numCounters * 50);
  }
} ///:~

Как и прежде, каждый счетчик содержит свой собственный компонент для отображения значения: два текстовых поля и надпись, первоначально показывающую что счетчики равны. Эти компоненты добавляются на панель родительского объекта в конструкторе TwoCounter. Так как два процесса начинают выполнение после нажатия пользователем кнопки, можно сделать так, чтобы start() мог быть вызван более одного раза. Так как Thread.start( ) не может быть вызван более одного раза для процесса (иначе генерируется исключение), то в приведенном алгоритме переопределен метод start() и используется флаг started.

В вызове run(), функции count1 и count2 увеличивают и отображают значение, так, что все кажется идентично. Затем вызывается sleep( ); без этого вызова программа "повиснет" поскольку CPU будет трудно переключаться между процессами.

Метод synchTest( ) выполняет очевидные функции по сравнению на равенство значения счетчиков count1 и count2; если они не равны то он установит значение надписи на панели в "Unsynched". Но в начале, он вызывает статический член класса Sharing1, который увеличит и отобразит значение счетчика доступа, чтобы показать сколько раз проверка закончилась успешно. (Причина использования данного счетчика будет понятна из следующих примеров.)

Класс Watcher является процессом, работа которого заключается в вызове synchTest() для всех активных объектов TwoCounter. Он выполняет это используя массив, хранящий объекты Sharing1. Можете считать, что Watcher постоянно читает объекты из TwoCounter.

Sharing1 содержит массив объектов TwoCounter инициализируемый при init() и запускаемый как процесс когда нажимается кнопка "start". Позже, когда будет нажата кнопка "Watch", создаются два или более наблюдателя и уничтожают ничего неподозревающие процессы TwoCounter.

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

<param name=size value="20">
<param name=watchers value="1">

Можете экспериментировать изменяя значение высоты и ширины и прочие параметры. Изменяя size и watchers вы изменяете поведение программы. Данная программа настроена на выполнение как одиночное приложение с передачей всех параметров через командную строку (или с использованием значений по умолчанию).

А вот и наиболее интересная часть. В вызове TwoCounter.run(), бесконечный цикле просто повторяет следующие строки:

t1.setText(Integer.toString(count1++));
t2.setText(Integer.toString(count2++));

(так же как и sleep, но здесь это не важно). Однако, когда программа будет запущена, вы увидите, что значения count1 и count2 будут временами различны (что покажет Watcher)! Это связано с особенностями процесса, он может быть временно приостановлен  в любое время. Таким образом в то время, когда приостановка произошла при выполнение двух приведенных выше строк, а процесс Watcher произвел сравнение как раз в это время, то как раз два счетчика и будут различны.

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

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

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

Как Java получает доступ к ресурсам

В Java есть встроенная поддержка предотвращения коллизий при использовании одного типа ресурсов - объектов в памяти. Поскольку элементы данных класса объявляются как private и доступ к этой области памяти возможен только посредством методов, то можно избежать коллизий объявив эти методы как synchronized. Одновременно только один процесс может вызвать synchronized метод для определенного объекта (хотя этот процесс может вызывать более одного синхронизированного метода объекта). Ниже приведены простые synchronized методы:
synchronized void f() { /* ... */ }
synchronized void g(){ /* ... */ }


Каждый объект имеет простой  замок (также называемый  monitor), который является автоматической частью объекта (т.е. нет необходимости писать специальный код). При вызове любого synchronized метода, этот объект блокируется и ни один другой synchronized метод этого объекта не может быть вызван до тех пор, пока первый не закончиться и не разблокирует объект. В выше приведенном примере, если f() вызвана для объекта, то g() не может быть вызвана для того же объекта до тех, пока  f() не завершится и не снимет блокировку. Таким образом, это единственная (в смысле одна - Прим. пер.) блокировка, используемая всеми synchronized методами отдельного объекта и эта блокировка предотвращает возможность записи в память более чем одному методу в одно и тоже время (т.е. более одного процесса в одно и то же время).

Также существует по одной блокировке на каждый класс (как часть объекта Class для класса), таким образом методы  synchronized static могут заблокировать друг друга от одновременного доступа к static данным на много-классовой основе.

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

Синхронизация счетчиков

Вооружившись новым ключевым словом решим нашу задачу: мы просто добавляем ключевое слово  synchronized для методов в TwoCounter. Следующий пример такой же как и предыдущим, но добавлено одно ключевое слово:
//: c14:Sharing2.java
// Using the synchronized keyword to prevent
// multiple access to a particular resource.
// <applet code=Sharing2 width=350 height=500>
// <param name=size value="12">
// <param name=watchers value="15">
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

public class Sharing2 extends JApplet {
  TwoCounter[] s;
  private static int accessCount = 0;
  private static JTextField aCount = 
    new JTextField("0", 7);
  public static void incrementAccess() {
    accessCount++;
    aCount.setText(Integer.toString(accessCount));
  }
  private JButton 
    start = new JButton("Start"),
    watcher = new JButton("Watch");
  private boolean isApplet = true;
  private int numCounters = 12;
  private int numWatchers = 15;

  class TwoCounter extends Thread {
    private boolean started = false;
    private JTextField 
      t1 = new JTextField(5),
      t2 = new JTextField(5);
    private JLabel l = 
      new JLabel("count1 == count2");
    private int count1 = 0, count2 = 0;
    public TwoCounter() {
      JPanel p = new JPanel();
      p.add(t1);
      p.add(t2);
      p.add(l);
      getContentPane().add(p);
    }    
    public void start() {
      if(!started) {
        started = true;
        super.start();
      }
    }
    public synchronized void run() {
      while (true) {
        t1.setText(Integer.toString(count1++));
        t2.setText(Integer.toString(count2++));
        try {
          sleep(500);
        } catch(InterruptedException e) {
          System.err.println("Interrupted");
        }
      }
    }
    public synchronized void synchTest() {
      Sharing2.incrementAccess();
      if(count1 != count2)
        l.setText("Unsynched");
    }
  }
  
  class Watcher extends Thread {
    public Watcher() { start(); }
    public void run() {
      while(true) {
        for(int i = 0; i < s.length; i++)
          s[i].synchTest();
        try {
          sleep(500);
        } catch(InterruptedException e) {
          System.err.println("Interrupted");
        }
      }
    }
  }
  class StartL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      for(int i = 0; i < s.length; i++)
        s[i].start();
    }
  }
  class WatcherL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      for(int i = 0; i < numWatchers; i++)
        new Watcher();
    }
  }
  public void init() {
    if(isApplet) {
      String counters = getParameter("size");
      if(counters != null)
        numCounters = Integer.parseInt(counters);
      String watchers = getParameter("watchers");
      if(watchers != null)
        numWatchers = Integer.parseInt(watchers);
    }
    s = new TwoCounter[numCounters];
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    for(int i = 0; i < s.length; i++)
      s[i] = new TwoCounter();
    JPanel p = new JPanel();
    start.addActionListener(new StartL());
    p.add(start);
    watcher.addActionListener(new WatcherL());
    p.add(watcher);
    p.add(new Label("Access Count"));
    p.add(aCount);
    cp.add(p);
  }
  public static void main(String[] args) {
    Sharing2 applet = new Sharing2();
    // This isn't an applet, so set the flag and
    // produce the parameter values from args:
    applet.isApplet = false;
    applet.numCounters = 
      (args.length == 0 ? 12 :
        Integer.parseInt(args[0]));
    applet.numWatchers =
      (args.length < 2 ? 15 :
        Integer.parseInt(args[1]));
    Console.run(applet, 350, 
      applet.numCounters * 50);
  }
} ///:~

Можно заметить, что оба run() и synchTest() теперь synchronized.  Если синхронизировать только один из методов, то другой свободен в игнорировании блокировки объекта и может быть безнаказанно вызван. Это очень важное замечание: Каждый метод, который имеет доступ к критическим общим ресурсам должен быть synchronized, иначе он не будет правильно работать.

Теперь у программы появилось новое поведение. Watcher никогда не прочитает что происходит потому, что оба метода run() стали synchronized и, так как run() всегда запущен для каждого объекта, блокировка всегда установлена и synchTest() никогда не вызовется.  Это видно, так как accessCount никогда не меняется.

Что нам нравится в этом примере, так это возможность изолировать только часть кода внутри run(). Та часть кода, которую необходимо изолировать данным способ, называется  критическим участком (critical section) и используется ключевое слово synchronized, чтобы различными способами установить критические участки. Java поддерживает критические участки с помощью  синхронизированных блоков; в данном случае synchronized используется для определения объекта, блокировка которого будет использована для синхронизации прилагаемого кода:

synchronized(syncObject) {
  // This code can be accessed 
  // by only one thread at a time
}

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

Пример Sharing2 может быть изменен если убрать ключевое слово synchronized у обоих методов run() и, вместо этого, установить блок synnchronized вокруг двух критических строк кода. Но что объект должен использовать как блокировку? То что уже используется synchTest(), т.е. ткущий объект (this)! Таким образом измененный run() выглядит следующим образом:

  public void run() {
    while (true) {
      synchronized(this) {
        t1.setText(Integer.toString(count1++));
        t2.setText(Integer.toString(count2++));
      }
      try {
        sleep(500);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
    }
  }

Это единственные исправления которые необходимо сделать в Sharing2.java и, как видите, поскольку оба счетчика синхронизированы (согласно тому, что Watcher теперь может следить за ними), то Watcher получает соответствующий доступ во время выполнения run().

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

Эффективность синхронизации

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

Повторное обращение к JavaBeans

Теперь, после того как вы познакомились с синхронизацией, можете иначе взглянуть на JavaBeans. Когда бы вы не создавали Bean, вы должны предполагать, что он будет использован в среде с множеством процессов. Это значит, что:
  1. Везде, где это возможно, все public методы Bean должны быть synchronized. Конечно, это приведет к увеличению времени выполнения synchronized методов. Если это будет основной загвоздкой, то методы, не вызывающие подобных проблем в критических секциях должны быть оставлены без synchronized, но учитывайте, что обычно это не разрешается. synchronized должны быть методы, которые могут быть оценены как относительно небольшие (например getCircleSize() в следующем примере) и/или "атомарные", то есть те, которые вызывают такие небольшие куски кода, что объект не может быть изменен во время выполнения. Установка подобных методов как не-synchronized может не иметь какого-либо особенного эффекта на скорости выполнения программы. Вы можете также определить все public методы Bean как synchronized и опустить ключевое слово synchronized только тогда, когда вы твердо убеждены, что это необходимо и что это не приведет к изменениям.
  2. При выполнении множественных событий для нескольких слушателей заинтересованных в этом событии, необходимо предположить, что слушатели могут быть добавлены или удалены при перемещении через список.
Первый пункт совершенно прост для рассмотрения, но следующий требует некоторого обдумывания. Рассмотрим пример BangBean.java, приведенный в последней главе. Тогда мы ушли от ответа на вопрос о множестве процессов игнорированием ключевого слова synchronized (который не был еще объяснен) и сделав события одноадресные (unicast).  А вот тот же пример, измененный для работы в среде с множеством процессов и использованием многоадресных событий:
//: c14:BangBean2.java
// You should write your Beans this way so they 
// can run in a multithreaded environment.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.io.*;
import com.bruceeckel.swing.*;

public class BangBean2 extends JPanel 
    implements Serializable {
  private int xm, ym;
  private int cSize = 20; // Circle size
  private String text = "Bang!";
  private int fontSize = 48;
  private Color tColor = Color.red;
  private ArrayList actionListeners = 
    new ArrayList();
  public BangBean2() {
    addMouseListener(new ML());
    addMouseMotionListener(new MM());
  }
  public synchronized int getCircleSize() { 
    return cSize; 
  }
  public synchronized void 
  setCircleSize(int newSize) {
    cSize = newSize;
  }
  public synchronized String getBangText() { 
    return text; 
  }
  public synchronized void 
  setBangText(String newText) {
    text = newText;
  }
  public synchronized int getFontSize() { 
    return fontSize; 
  }
  public synchronized void 
  setFontSize(int newSize) {
    fontSize = newSize;
  }
  public synchronized Color getTextColor() {
    return tColor; 
  }
  public synchronized void 
  setTextColor(Color newColor) {
    tColor = newColor;
  }
  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    g.setColor(Color.black);
    g.drawOval(xm - cSize/2, ym - cSize/2, 
      cSize, cSize);
  }
  // This is a multicast listener, which is
  // more typically used than the unicast
  // approach taken in BangBean.java:
  public synchronized void 
    addActionListener(ActionListener l) {
    actionListeners.add(l);
  }
  public synchronized void 
    removeActionListener(ActionListener l) {
    actionListeners.remove(l);
  }
  // Notice this isn't synchronized:
  public void notifyListeners() {
    ActionEvent a =
      new ActionEvent(BangBean2.this,
        ActionEvent.ACTION_PERFORMED, null);
    ArrayList lv = null;
    // Make a shallow copy of the List in case 
    // someone adds a listener while we're 
    // calling listeners:
    synchronized(this) {
      lv = (ArrayList)actionListeners.clone();
    }
    // Call all the listener methods:
    for(int i = 0; i < lv.size(); i++)
      ((ActionListener)lv.get(i))
        .actionPerformed(a);
  }
  class ML extends MouseAdapter {
    public void mousePressed(MouseEvent e) {
      Graphics g = getGraphics();
      g.setColor(tColor);
      g.setFont(
        new Font(
          "TimesRoman", Font.BOLD, fontSize));
      int width = 
        g.getFontMetrics().stringWidth(text);
      g.drawString(text, 
        (getSize().width - width) /2,
        getSize().height/2);
      g.dispose();
      notifyListeners();
    }
  }
  class MM extends MouseMotionAdapter {
    public void mouseMoved(MouseEvent e) {
      xm = e.getX();
      ym = e.getY();
      repaint();
    }
  }
  public static void main(String[] args) {
    BangBean2 bb = new BangBean2();
    bb.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e){
        System.out.println("ActionEvent" + e);
      }
    });
    bb.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e){
        System.out.println("BangBean2 action");
      }
    });
    bb.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e){
        System.out.println("More action");
      }
    });
    Console.run(bb, 300, 300);
  }
} ///:~

Добавление synchronized для методов есть простейшее изменение. Однако помня, что addActionListener( ) иremoveActionListener( ), которые относятся к ActionListener теперь добавлены в и удалены из ArrayList, так, что можно создать необходимое количество.

Можно видеть, что метод notifyListeners( ) не synchronized. Он может быть вызван из более чем одного процесса за раз. Также возможно для addActionListener( ) или removeActionListener( ) быть вызванными из самого вызова notifyListeners( ), что является проблемой поскольку он пересекается (traverse) в ArrayList actionListeners. Чтобы избежать этой проблемы ArrayList клонирован вне секции synchronized и клон пересечен (traversed) (в Приложении A объясняются детали клонирования). Таким образом оригинальный ArrayList может быть использован без воздействия на notifyListeners( ).

Метод paintComponent( ) также не synchronized. Решение, стоит ли синхронизировать переопределенный (overridden) метод не такое же простое как в случае когда добавляется собственный метод. В данном примере кажется, что paint() выполняется успешно, независимо от того синхронизирован он или нет. Но дополнительно необходимо рассмотреть:

  1. Изменяет ли метод значения "критических" переменных внутри объекта? Чтобы определить, является ли переменные "критическими", необходимо определить будут ли значения прочитаны или установлены другими процессами в программе. (В этом случае чтение и установка значения фактически всегда происходит через synchronized методы, так что можно их просто проверить.) В случае с paint() ни каких изменений нет.
  2. Зависит ли метод от значения этих "критических" переменных? Если synchronized метод изменяет значение той переменной, которую использует ваш метод, то вам просто необходимо также объявить ваш метод как synchronized.  В связи с этим, можно видеть, что значение переменной cSize изменяется synchronized методами и, следовательно, paint() также должен быть synchronized. Однако в данном случае можно спросить, "А что ужасного произойдет в том случае, если cSize измениться во время paint()?" Когда видно, что ничего плохого, к тому же присутствует эффект самовосстановления (transient effect), можно решить оставить paint() не synchronized во избежании излишних накладных расходов при вызове synchronized метода.
  3. И в третьих, необходимо убедиться, является ли базовый класс для paint() synchronized или нет. Это не просто высказывание для сотрясания воздуха, а просто подсказка. В нашем случае например, поля, изменяемые через synchronized методы (такие как cSize), были перемешены в paint() формуле и могли изменить ситуацию. Однако обратите внимание, что synchronized не наследуется, так например, если метод является synchronized в базовом классе, то он не будет автоматически synchronized в переопределенном методе наследующего класса.
Тестовая программа TestBangBean2 была изменена по сравнению с версией из предыдущей главы так, чтобы показать способность множественного приведение типов в BangBean2 через добавление дополнительных слушателей.

Блокировки

Процесс может быть в одном из четырех состояний:
  1.  New: Процесс был создан, но не был еще запущен, так что он не может выполняться.
  2.  Runnable: Это означает, что процесс может быть выполнен когда механизм распределения квантов времени CPU даст возможность выполняться процессу. Так, процесс может, а может и не быть выполняемым, но ему ни чего не препятствует  быть выполняемым в том момент, когда пришла его очередь (квант времени); он не мертв и не заблокирован.
  3.  Dead: Нормальный способ процесса завершиться является возврат из его run() метода. Можно также  вызвать stop( ), но это вызовет исключение являющееся подклассом Error (что означает, что вы не поместили вызов в блок try). Помните, что генерация исключения должно быть специальным событием и не является частью нормального хода выполнения программы; так, использование stop() запрещено (deprecated) в Java2. Также существует  метод destroy() (который ни когда не был реализован), который вы не должны вызывать если можно этого избежать поскольку это радикальное решение и не снимает блокировку объекта.
  4.  Blocked: Процесс может быть запущен, но не будет выполняться. Пока процесс находиться в блокированном состоянии планировщик просто пропускает его и не выделяет квантов времени. До тех пор, пока процесс не перейдет в состояние runnable, процесс не выполнит ни одной операции.

Установка блокировки

Блокированное состояние одно из наиболее интересных и стоит последующего рассмотрения. Процесс может стать блокированным в пяти случаях:
  1. Установка процесса в спящее состояние посредством вызова sleep(milliseconds), в этом случае он не будет выполняться определенный промежуток времени.
  2. Приостановка выполнения процесса вызовом suspend( ). Он не будет выполняться до тех пор, пока не получит сообщение resume( ) (что запрещено в Java 2, и дальше будет описано).
  3. Приостановка выполнения с помощью wait( ). Процесс не будет повторно запущен на выполнение до тех, пор пока не получит сообщение notify( ) или notifyAll( ). (Это похоже на пункт 2, но существуют определенные различия, которые будут также показаны.)
  4. Процесс ожидает завершения каких-то операций ввода/вывода.
  5. Процесс пытается вызвать synchronized метод другого объекта и блокировка этого объекта невозможна.

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

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

В связи с тем, что данный пример показывает некоторые запрещенные (deprecated) при компиляции будет выдано соответствующее сообщение.

В начале основная программы:

//: c14:Blocking.java
// Demonstrates the various ways a thread
// can be blocked.
// <applet code=Blocking width=350 height=550>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import com.bruceeckel.swing.*;

//////////// The basic framework ///////////
class Blockable extends Thread {
  private Peeker peeker;
  protected JTextField state = new JTextField(30);
  protected int i;
  public Blockable(Container c) {
    c.add(state);
    peeker = new Peeker(this, c);
  }
  public synchronized int read() { return i; }
  protected synchronized void update() {
    state.setText(getClass().getName()
      + " state: i = " + i);
  }
  public void stopPeeker() { 
    // peeker.stop(); Deprecated in Java 1.2
    peeker.terminate(); // The preferred approach
  }
}

class Peeker extends Thread {
  private Blockable b;
  private int session;
  private JTextField status = new JTextField(30);
  private boolean stop = false;
  public Peeker(Blockable b, Container c) {
    c.add(status);
    this.b = b;
    start();
  }
  public void terminate() { stop = true; }
  public void run() {
    while (!stop) {
      status.setText(b.getClass().getName()
        + " Peeker " + (++session)
        + "; value = " + b.read());
       try {
        sleep(100);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
    }
  }
} ///:Continued

Предполагается, что класс Blockable будет базовым для всех остальных классов в данном примере, который демонстрирует блокировку. Объект Blockable содержит JTextField называемое state, используемое для показа информации об объекте. Метод, который выводит эту информацию - update().  Как видно, он использует getClass().getName() для получения имени класса вместо простого его вывода; это сделанно из-за того, что update() не может знать действительное имя объекта вызвавшего его поскольку этот класс наследник от Blockable.

int i это индикатор изменений в Blockable, который увеличивает свое значение через метод run() наследуемого класса.

Также есть процесс класса Peeker, который запускается для каждого объекта Blockable и его работа заключается в наблюдении за изменением переменной i в ассоциированном с ним объекте Blockable через вызов read() и выводом значения в его status JTextField поле. Вот что важно: оба метода read() и update() являются synchronized, что означает необходимость в отсутствии блокировки объекта для их выполнения.

Засыпание

Первый тест в этой программе sleep( ):
///:Continuing
///////////// Blocking via sleep() ///////////
class Sleeper1 extends Blockable {
  public Sleeper1(Container c) { super(c); }
  public synchronized void run() {
    while(true) {
      i++;
      update();
       try {
        sleep(1000);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
    }
  }
}
  
class Sleeper2 extends Blockable {
  public Sleeper2(Container c) { super(c); }
  public void run() {
    while(true) {
      change();
       try {
        sleep(1000);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
    }
  }
  public synchronized void change() {
      i++;
      update();
  }
} ///:Continued

В Sleeper1 весь метод run( ) объявлен как synchronized. Можно видеть, что ассоциированный с этим объектом Peeker весело выполняется до тех пор, пока вы не запустите процесс, после чего Peeker замораживается. Это одна из форм блокировки: поскольку Sleeper1.run() объявлен synchronized, а как только процесс запускается  он всегда находиться внутри run(), то метод никогда не снимет блокировку объекта и Peeker блокирован.

Sleeper2 предоставляет решение сделав run() не-synchronized. Только метод change() объявлен как synchronized, что означает, что пока run() в sleep(), Peeker может получить доступ к необходимым ему synchronized методам, в данном случае read(). И в данном случае видно, что Peeker продолжает выполняться и после старта процесса Sleeper2.

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

Следующая часть примера демонстрирует понятие приостановки. В класс Thread присутствует метод suspend( ) для временной остановки процесса и метод  resume( ), перезапускающий процесс с той же самой точки где он был остановлен. Метод resume() должен быть вызван каким-либо процессом из вне, и в данном случае мы имеем отдельный класс названный Resumer, которые это и делает. Каждый класс, демонстрирующий приостановку/возабновление имеет свой собственный Resumer:
///:Continuing
/////////// Blocking via suspend() ///////////
class SuspendResume extends Blockable {
  public SuspendResume(Container c) {
    super(c);    
    new Resumer(this); 
  }
}

class SuspendResume1 extends SuspendResume {
  public SuspendResume1(Container c) { super(c);}
  public synchronized void run() {
    while(true) {
      i++;
      update();
      suspend(); // Deprecated in Java 1.2
    }
  }
}

class SuspendResume2 extends SuspendResume {
  public SuspendResume2(Container c) { super(c);}
  public void run() {
    while(true) {
      change();
      suspend(); // Deprecated in Java 1.2
    }
  }
  public synchronized void change() {
      i++;
      update();
  }
}

class Resumer extends Thread {
  private SuspendResume sr;
  public Resumer(SuspendResume sr) {
    this.sr = sr;
    start();
  }
  public void run() {
    while(true) {
       try {
        sleep(1000);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
      sr.resume(); // Deprecated in Java 1.2
    }
  }
} ///:Continued

SuspendResume1 также имеет метод synchronized run( ). Еще раз, когда будет запущен данный процесс, то видно, что ассоциированный с ним блокированный Peeker ожидает блокировки, чего ни когда не произойдет. Это устраняется, как и прежде, в SuspendResume2, у которого не синхронизирован весь метод run(), но вместо этого, используется отдельный синхронизированный метод change( ).

Вы должны быть осведомлены о том, что в Java2 не разрешено (deprecated) использовать suspend() и resume(), так как suspend() захватывает блокировку объекта и поэтому может возникнуть зависание (deadlock-prone). Таким образом можно запросто прийти к ситуации, когда имеется несколько объектов, ожидающих друг друга, что вызовет подвисание программы. Хотя вы и можете увидеть их использование в старых программах вы не должны использовать suspend() и resume(). Более подходящее решение будет описано в данной главе несколько позднее.

Ожидание и уведомление

Из первых двух примеров очень важно понять, как sleep(), так и suspend() не освобождают блокировку во время своего вызова. Вы должны знать об этом когда работает с блокировками. С другой стороны, методwait( ) освобождает блокировку во время своего вызова, что означает, что другие,  synchronized методы в объекте процесса могут быть вызваны во время wait(). В следующих двух классах видно, что метод run() полностью synchronized в обоих классах, однако Peeker все также имеет полный доступ к synchronized методам во время wait(). Это происходит из-за того, что wait() освобождает блокировку объекта после приостановки метода из которого он вызван.

Также видно, что существуют две формы wait(). Первая принимает аргумент в миллисекундах, что имеет то же значение как и в sleep(): остановку на это время. Различие в том, что в wait() блокировка объекта освобождается и вы можете выйти из wait() с помощью notify() так же как и после истечения времени.

Вторая форма без передачи параметров означает, что wait() будет выполняться до тех пор пока не будет вызвано notify() и не остановится автоматически по истечению времени.

Один, довольно уникальный аспект wait( ) и notify( ) в том, что оба метода являются частью базового класса Object, а не частью Thread, как sleep( ), suspend( ) и resume( ). Хотя это и выглядит немного странно в начале - сделать то, что должно относиться исключительно к процессу доступным для базового класса - это необходимо, так как он управляет блокировками, которые являются частью каждого объекта. В результате можно поместить wait() в любой syncronized метод, в зависимости от того, будет ли какой-либо процесс выполнять именно данный класс. Фактически, единственное применение для wait() быть вызванным из synchronized метода или блокировки. Если вызвать wait() или notify() в необъявленном как synchronuzed методе, то программа будет прекрасно компилироваться, но когда вы ее запустите, то получите IllegalMonitorStateException с каким-то не сразу понятным сообщением "current thread not owner" (текущий процесс не владелец). Запомните, что sleep(), suspend() и resume() могут быть вызваны из не-syncronized методов, поскольку они не управляют блокировкой.

Вы можете вызвать wait() или notify() только для вашей собственной блокировки. Еще раз, вы сможете скомпилировать код, который пытается использовать неверную блокировку, но это приведет вас к тому самому IllegalMonitorStateException сообщению как и прежде. Также ни чего не получиться с чужой блокировкой, но можно попросить другой объект выполнить операцию с его собственной блокировкой. Таким образом одна из попыток заключается в создании syncronized метода, который вызывает notify() для своего собственного объекта. Однако в Notifier видим вызов notify() из syncronized блока:

synchronized(wn2) {
  wn2.notify();
}
где wn2 объекта типа WaitNotify2. Этот, не являющийся частью WaitNotifier2, метод, имеет  блокировку на объект wn2 и с этого момента он совершенно спокойно может вызвать notify() для wn2 и не получить IllegalMonitorStateException.
///:Continuing
/////////// Blocking via wait() ///////////
class WaitNotify1 extends Blockable {
  public WaitNotify1(Container c) { super(c); }
  public synchronized void run() {
    while(true) {
      i++;
      update();
       try {
        wait(1000);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
    }
  }
}

class WaitNotify2 extends Blockable {
  public WaitNotify2(Container c) {
    super(c);
    new Notifier(this); 
  }
  public synchronized void run() {
    while(true) {
      i++;
      update();
       try {
        wait();
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
    }
  }
}

class Notifier extends Thread {
  private WaitNotify2 wn2;
  public Notifier(WaitNotify2 wn2) {
    this.wn2 = wn2;
    start();
  }
  public void run() {
    while(true) {
       try {
        sleep(2000);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
      synchronized(wn2) {
        wn2.notify();
      }
    }
  }
}  ///:Continued
wait( ) обычно используется тогда, когда вы пришли к той точке программы, в которой вы ожидаете каких-либо других состояний, изменяемых под воздействием из вне вашего процесса, и не хотите пустого ожидания внутри вашего процесса.  То есть wait() позволяет вам перевести процесс в сонное состояние в ожидании изменения мира и его сможет разбудить только notify() или notifyAll(), после чего он проснется и посмотрит что изменилось. Таким образом обеспечивается способ синхронизации между процессами.

Блокировка во время операций ввода/вывода

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

Sender помещает данные в Writer и засыпает на случайный промежуток времени. Однако, Receiver не имеет sleep(), suspend() или wait() и когда происходит вызов read() он автоматически блокируется до тех пор пока есть данные.

///:Continuing
class Sender extends Blockable { // send
  private Writer out;
  public Sender(Container c, Writer out) { 
    super(c);
    this.out = out; 
  }
  public void run() {
    while(true) {
      for(char c = 'A'; c <= 'z'; c++) {
        try {
          i++;
          out.write(c);
          state.setText("Sender sent: " 
            + (char)c);
          sleep((int)(3000 * Math.random()));
        } catch(InterruptedException e) {
          System.err.println("Interrupted");
        } catch(IOException e) {
          System.err.println("IO problem");
        }
      }
    }
  }
}

class Receiver extends Blockable {
  private Reader in;
  public Receiver(Container c, Reader in) { 
    super(c);
    this.in = in; 
  }
  public void run() {
    try {
      while(true) {
        i++; // Show peeker it's alive
        // Blocks until characters are there:
        state.setText("Receiver read: "
          + (char)in.read());
      }
    } catch(IOException e) {
      System.err.println("IO problem");
    }
  }
} ///:Continued
Оба класса также помещают информацию в их поле state и изменяют значение i, так что Peeker контролирует выполнение процессов. 

Тестирование

Главный класс апплета на удивление простой, потому что основной код перемещен в Blockable. В основном создаются массивы объектов Blockable, и, поскольку каждый из есть процесс, то каждый выполняют свою работу  когда вы нажимаете кнопку "start". Есть также кнопка и ее actionPerformed() для остановки всех объектов Peekers, демонстрирующая альтернативу вызову запрещенному (в Java 2) методу stop() для Thread.

Для установления соединения между объектами Sender и Reciever создаются PipedWriter и PipedReader. Учтите, что PipedReaderin должен быть соединен с PipedWriteout через аргумент конструктора. После этого, все, что помещается в out в скором времени должно быть получено из in, так, как если бы это было отправлено через pipe (трубу, в соответствии с названием). Объекты in и out далее передаются конструкторам Receiver и Sender соответственно, которые расценивают их как оъекты Reader и Writer для различных типов. (таким образом, их можно привести  к любому типу).

Массив указателей b типа Blockable не инициализируется определениями в этом месте поскольку потоки не могут быть установлены до их описания (необходимость блока try предотвращает это).

///:Continuing
/////////// Testing Everything ///////////
public class Blocking extends JApplet {
  private JButton 
    start = new JButton("Start"),
    stopPeekers = new JButton("Stop Peekers");
  private boolean started = false;
  private Blockable[] b;
  private PipedWriter out;
  private PipedReader in;
  class StartL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(!started) {
        started = true;
        for(int i = 0; i < b.length; i++)
          b[i].start();
      }
    }
  }
  class StopPeekersL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      // Demonstration of the preferred 
      // alternative to Thread.stop():
      for(int i = 0; i < b.length; i++)
        b[i].stopPeeker();
    }
  }
  public void init() {
     Container cp = getContentPane();
     cp.setLayout(new FlowLayout());
     out = new PipedWriter();
    try {
      in = new PipedReader(out);
    } catch(IOException e) {
      System.err.println("PipedReader problem");
    }
    b = new Blockable[] {
      new Sleeper1(cp),
      new Sleeper2(cp),
      new SuspendResume1(cp),
      new SuspendResume2(cp),
      new WaitNotify1(cp),
      new WaitNotify2(cp),
      new Sender(cp, out),
      new Receiver(cp, in)
    };
    start.addActionListener(new StartL());
    cp.add(start);
    stopPeekers.addActionListener(
      new StopPeekersL());
    cp.add(stopPeekers);
  }
  public static void main(String[] args) {
    Console.run(new Blocking(), 350, 550);
  }
} ///:~

В init() обратите внимание на цикл, проходящий по всему массиву и добавляющий state и текстовое поле peeker.status на страницу.

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

Мертвая блокировка

Из-за того, что процесс может быть блокирован и из-за того, что объекты могут иметь synchronized методы, запрещающие процессам доступ к этим объектам до тех пор, пока не будет снята блокировка синхронизации, то возможен случай, когда один процесс ожидает другой процесс, который в свою очередь ожидает третий процесс и так далее, до тех пор пока цепочка не вернется к первому ожидающему процессу. В этом случае мы получаем бесконечный цикл процессов ожидающих друг друга, причем ни один не может продолжить выполнение. Это называется мертвая блокировка (deadlock). Можно утверждать, что это не происходит слишком часто, но когда это произойдет с вашим кодом, то будет очень сложно обнаружить ошибку.

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

Запрещение stop( ), suspend( ), 
resume( ), и destroy( ) в Java 2

Одно из изменений, которое было сделано в Java2 для уменьшения возможности возникновения мертвых блокировок заключалось в запрещении для Thread методов stop(), suspend(), resume() и destroy( ).

Причина, по которой метод stop() запрещен в том, что он не снимал блокировки полученные процессом и, если объект находился в неустойчивом состоянии ("разрушенный"), другие процессы могли просмотреть и изменить его состояние. Возникающие при этом проблемы с трудом могли быть определены. Вместо использования stop() лучше следовать примеру Blocking.java и использовать флаг для уведомления своего процесса о том, когда следует выйти из метода run().

Иногда процесс блокирован, например когда ожидает ввода, и не может просмотреть флаг как это сделано в Blocking.java. В этом случае также не следует использовать stop( ), а использовать вместо этого методinterrupt( ) в Thread для разрыва блокированного кода:

//: c14:Interrupt.java
// The alternative approach to using 
// stop() when a thread is blocked.
// <applet code=Interrupt width=200 height=100>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

class Blocked extends Thread {
  public synchronized void run() {
    try {
      wait(); // Blocks
    } catch(InterruptedException e) {
      System.err.println("Interrupted");
    }
    System.out.println("Exiting run()");
  }
}

public class Interrupt extends JApplet {
  private JButton 
    interrupt = new JButton("Interrupt");
  private Blocked blocked = new Blocked();
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(interrupt);
    interrupt.addActionListener(
      new ActionListener() {
        public 
        void actionPerformed(ActionEvent e) {
          System.out.println("Button pressed");
          if(blocked == null) return;
          Thread remove = blocked;
          blocked = null; // to release it
          remove.interrupt();
        }
      });
    blocked.start();
  }
  public static void main(String[] args) {
    Console.run(new Interrupt(), 200, 100);
  }
} ///:~


Метод wait() внутри Blocked.run() блокирует процесс. Когда вы нажимаете кнопку, ссылка blocked установлена в null, так что сборщик мусора удаляет ее, после чего для этого объекта вызывается метод interrupt(). Первый раз когда вы нажимаете кнопку видно, что процесс завершается, когда процессов для завершения не останется кнопка останется в нажатом состоянии.

Методы suspend() и resume() по умолчанию являются склонными к созданию мертвых блокировок. Когда вызывается suspend() целевой процесс останавливается, но он все равно может получить блокировку установленную в этот момент. Таким образом, ни один ни другой процесс не сможет получить доступ к блокированным ресурсам пока процесс не разблокируется. Любой процесс, который хочет разблокировать целевой процесс и также пытается использовать любой из заблокированных ресурсов приведет к мертвой блокировке. Вы не должны использовать suspend() и resume(), а вместо этого следует установить флаг в ваш класс Thread для отображения того факта должен ли быть процесс активным или временно приостановлен.  процесс переход в ожидание используя wait(). Когда флаг показывает, что процесс должен быть возобновлен процесс перезапускается с помощью notify(). Пример может быть создан с помощью переделки Counter2.java. Хотя эффект одинаков, можно заметить, что сам код совершенно отличен ╒ анонимные внутренние классы используются для всех слушателей, а также Thread является внутренним классом, что делает программирование немного более удобным поскольку это предотвращает учета дополнительно использованных системных ресурсов необходимых в Counter2.java:

//: c14:Suspend.java
// The alternative approach to using suspend()
// and resume(), which are deprecated in Java 2.
// <applet code=Suspend width=300 height=100>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

public class Suspend extends JApplet {
  private JTextField t = new JTextField(10);
  private JButton 
    suspend = new JButton("Suspend"),
    resume = new JButton("Resume");
  private Suspendable ss = new Suspendable();
  class Suspendable extends Thread {
    private int count = 0;
    private boolean suspended = false;
    public Suspendable() { start(); }
    public void fauxSuspend() { 
      suspended = true;
    }
    public synchronized void fauxResume() {
      suspended = false;
      notify();
    }
    public void run() {
      while (true) {
        try {
          sleep(100);
          synchronized(this) {
            while(suspended)
              wait();
          }
        } catch(InterruptedException e) {
          System.err.println("Interrupted");
        }
        t.setText(Integer.toString(count++));
      }
    }
  } 
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(t);
    suspend.addActionListener(
      new ActionListener() {
        public 
        void actionPerformed(ActionEvent e) {
          ss.fauxSuspend();
        }
      });
    cp.add(suspend);
    resume.addActionListener(
      new ActionListener() {
        public 
        void actionPerformed(ActionEvent e) {
          ss.fauxResume();
        }
      });
    cp.add(resume);
  }
  public static void main(String[] args) {
    Console.run(new Suspend(), 300, 100);
  }
} ///:~

Флаг suspended внутри Suspendable используется для включения или отключения временной приостановки. Для приостановки флаг устанавливается в true через вызов fauxSuspend() и это определяется внутри run(). wait(), как было описано в этом разделе раннее, должен быть synchronized, так что он может иметь блокировку объекта. В fauxResume(), флаг suspended устанавливается в false и вызывается notify(), поскольку это разбудит wait() внутри блока synchronized, то метод fauxRsume() должен быть также объявлен как synchronized так, что он получает блокировку до вызова notify() (таким образом блокировка доступна для wait() чтобы проснуться). Если следовать стилю этой программы, то можно избежать использования suspend() и resume().

Метод destroy( ) для Thread никогда не будет реализован; это аналогично suspend() который не может продолжить выполнение, и поэтому он имеет те же самые склонности к мертвой блокировке как и suspend(). Однако это не запрещенный (deprecated) метод и может быть реализован в следующих версиях Java (после 2) для специальных ситуаций, в которых риск мертвой блокировки приемлем.

Можно удивляться, почему эти методы, в настоящее время запрещенные, были включены в Java в начале. Похоже была допущена довольно существенная ошибка чтобы просто полностью убрать их (и сделать еще один прокол в аргументации об особенном дизайне Java и в агитации безотказной работы меркетологами Sun). Слова же в поддержку изменений заключается в том, что это ясно показывает, что программисты, а не маркетологи играют в спектакль - одни находят проблемы, другие исправляют их. Я считаю это более перспективным и обнадеживающим, чем уход от проблемы только из-за того, что "исправление ошибки приводи к ошибке". Это также означает, что Java продолжает улучшаться, даже если это вызывает дискомфорт у части Java программистов. Уж лучше я буду испытывать временный дискомфорт чем наблюдать застой языка.

Приоритеты

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

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

Чтение и установка приоритетов

Можно определить приоритет процесса с помощью getPriority( ) и изменить его  setPriority( ). Форму предыдущих примеров счетчиков "counter" можно использовать для демонстрации эффекта изменния приоритетов. В данном апплете можно видеть как счетчик замедляется по мере того, как его процесс получает низший приоритет:
//: c14:Counter5.java
// Adjusting the priorities of threads.
// <applet code=Counter5 width=450 height=600>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

class Ticker2 extends Thread {
  private JButton
    b = new JButton("Toggle"),
    incPriority = new JButton("up"),
    decPriority = new JButton("down");
  private JTextField
    t = new JTextField(10),
    pr = new JTextField(3); // Display priority
  private int count = 0;
  private boolean runFlag = true;
  public Ticker2(Container c) {
    b.addActionListener(new ToggleL());
    incPriority.addActionListener(new UpL());
    decPriority.addActionListener(new DownL());
    JPanel p = new JPanel();
    p.add(t);
    p.add(pr);
    p.add(b);
    p.add(incPriority);
    p.add(decPriority);
    c.add(p);
  }
  class ToggleL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      runFlag = !runFlag;
    }
  }
  class UpL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      int newPriority = getPriority() + 1;
      if(newPriority > Thread.MAX_PRIORITY)
        newPriority = Thread.MAX_PRIORITY;
      setPriority(newPriority);
    }
  }
  class DownL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      int newPriority = getPriority() - 1;
      if(newPriority < Thread.MIN_PRIORITY)
        newPriority = Thread.MIN_PRIORITY;
      setPriority(newPriority);
    }
  }
  public void run() {
    while (true) {
      if(runFlag) {
        t.setText(Integer.toString(count++));
        pr.setText(
          Integer.toString(getPriority()));
      }
      yield();
    }
  }
}

public class Counter5 extends JApplet {
  private JButton
    start = new JButton("Start"),
    upMax = new JButton("Inc Max Priority"),
    downMax = new JButton("Dec Max Priority");
  private boolean started = false;
  private static final int SIZE = 10;
  private Ticker2[] s = new Ticker2[SIZE];
  private JTextField mp = new JTextField(3);
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    for(int i = 0; i < s.length; i++)
      s[i] = new Ticker2(cp);
    cp.add(new JLabel(
      "MAX_PRIORITY = " + Thread.MAX_PRIORITY));
    cp.add(new JLabel("MIN_PRIORITY = "
      + Thread.MIN_PRIORITY));
    cp.add(new JLabel("Group Max Priority = "));
    cp.add(mp);
    cp.add(start);
    cp.add(upMax);
    cp.add(downMax);
    start.addActionListener(new StartL());
    upMax.addActionListener(new UpMaxL());
    downMax.addActionListener(new DownMaxL());
    showMaxPriority();
    // Recursively display parent thread groups:
    ThreadGroup parent =
      s[0].getThreadGroup().getParent();
    while(parent != null) {
      cp.add(new Label(
        "Parent threadgroup max priority = "
        + parent.getMaxPriority()));
      parent = parent.getParent();
    }
  }
  public void showMaxPriority() {
    mp.setText(Integer.toString(
      s[0].getThreadGroup().getMaxPriority()));
  }
  class StartL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(!started) {
        started = true;
        for(int i = 0; i < s.length; i++)
          s[i].start();
      }
    }
  }
  class UpMaxL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      int maxp =
        s[0].getThreadGroup().getMaxPriority();
      if(++maxp > Thread.MAX_PRIORITY)
        maxp = Thread.MAX_PRIORITY;
      s[0].getThreadGroup().setMaxPriority(maxp);
      showMaxPriority();
    }
  }
  class DownMaxL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      int maxp =
        s[0].getThreadGroup().getMaxPriority();
      if(--maxp < Thread.MIN_PRIORITY)
        maxp = Thread.MIN_PRIORITY;
      s[0].getThreadGroup().setMaxPriority(maxp);
      showMaxPriority();
    }
  }
  public static void main(String[] args) {
    Console.run(new Counter5(), 450, 600);
  }
} ///:~

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

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

Метод init( ) в Counter5 создает массив из десяти Ticker2, их кнопки и поля ввода размещаются на форме конструктором Ticker2. Counter5 добавляет кнопки для общего запуска, а также кнопки для увеличения и уменьшения максимального значения приоритета для группы процессов. Добавочно существуют строки (label), для отображения возможных максимальных и минимальных значений приоритетов для процесса и JTextField, для отображения максимального приоритета для группы (мы рассмотрим группу процессов в следующем разделе). В заключении всего, приоритеты групп процессов потомков также отображаются как строки (labels).

Когда нажимается кнопка "up" или "down", то выбирается приоритет этого Ticker2 и он, соответственно, увеличивается или уменьшается.

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

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

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

Группы процессов

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

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

Причина существования групп процессов трудно понять из литературы, которые обычно не четко описывают данную область. Чаще всего цитируется "по причине защиты". Соглачно Arnold & Gosling,[71] "Threads within a thread group can modify the other threads in the group, including any farther down the hierarchy. A thread cannot modify threads outside of its own group or contained groups." (Процессы в группе могут изменять другие процессы этой группы, включая все последующие согласно иерархии. Процесс не может изменять процессы не входящие в его группу или группы в его группе). Довольно трудно понять, что означает "изменять" в приведенной цитате. Следующий пример показывает процесс в подгруппе "leaf", который изменяет приоритеты всех процессов в его дереве группы процессов, а также и сам метод, вызываемый для всех процессов в дереве.

//: c14:TestAccess.java
// How threads can access other threads
// in a parent thread group.

public class TestAccess {
  public static void main(String[] args) {
    ThreadGroup 
      x = new ThreadGroup("x"),
      y = new ThreadGroup(x, "y"),
      z = new ThreadGroup(y, "z");
    Thread
      one = new TestThread1(x, "one"),
      two = new TestThread2(z, "two");
  }
}

class TestThread1 extends Thread {
  private int i;
  TestThread1(ThreadGroup g, String name) {
    super(g, name);
  }
  void f() {
    i++; // modify this thread
    System.out.println(getName() + " f()");
  }
}

class TestThread2 extends TestThread1 {
  TestThread2(ThreadGroup g, String name) {
    super(g, name);
    start();
  }
  public void run() {
    ThreadGroup g =
      getThreadGroup().getParent().getParent();
    g.list();
    Thread[] gAll = new Thread[g.activeCount()];
    g.enumerate(gAll);
    for(int i = 0; i < gAll.length; i++) {
      gAll[i].setPriority(Thread.MIN_PRIORITY);
      ((TestThread1)gAll[i]).f();
    }
    g.list();
  }
} ///:~

В main() создается несколько ThreadGroup накладываясь друг на друга: х не имеет аргументов, за исключением своего имени (String) , так что он автоматически помещается в "системную" группу процессов, до тех пор пока y меньше х и z меньше у. Обратите внимание, что инициализация происходит в той же последовательности как написано, так что этот код правилен.

Два процесса создаются и помещаются в разные группы процессов. TestThread1 не имеет метода run(), но имеет метод f(), который изменяет процесс и выводит сообщение, чтобы вы знали, что он был вызван. TestThread2 является подклассом TestThread1 и его run() довольно сложен. В начале он определяет группу процессов текущего процесса, затем перемещается по дереву наследования на два уровня используя getParent(). (Это задумано поскольку я специально поместил объект TestThread2 на два уровня ниже по иерархии.) В этом месте создается массив ссылок на Thread используя метод activeCount(), чтобы знать, сколько процессов в данной группе и во всех подгруппах. Метод enumerate() помещает ссылки на все процессы в массив gAll, а затем я просто перемещаюсь по всему массиву вызывая метод f() для каждого процесса, заодно меняя приоритет. Таким образом, процесс в группе "leaf" изменяет процессы в группах родителя.

Отладочный метод list() выводит всю информацию о группе процессов на стандартный вывод, что полезно при изучении поведения процессов. Ниже приведена работа программы:

java.lang.ThreadGroup[name=x,maxpri=10]
    Thread[one,5,x]
    java.lang.ThreadGroup[name=y,maxpri=10]
        java.lang.ThreadGroup[name=z,maxpri=10]
            Thread[two,5,z]
one f()
two f()
java.lang.ThreadGroup[name=x,maxpri=10]
    Thread[one,1,x]
    java.lang.ThreadGroup[name=y,maxpri=10]
        java.lang.ThreadGroup[name=z,maxpri=10]
            Thread[two,1,z]

Метод list() не только выводит имя класса для ThreadGroup или Thread, но также и имя группы и ее максимальный приоритет. Для процессов имя процесса выводится после приоритета и имени группы, к которой он принадлежит. Обратите внимание, что list() вставляет отступы для процессов и групп процессов, чтобы показать, что они являются дочерни по отношении к группе без отступа.

Можно видеть, что f() вызывается методом run() из TestThread2, так что совершенно очевидно, что все процессы в группе уязвимы (vulnerable). Однако доступ возможен только к процессам являющимися подветвью вашей системной группы процессов и, вероятно, это и подразумевают под "безопастностью". Доступ к чужим системным группам не возможен.

Управление группами процессов

Возвращаясь к обсуждению темы безопасности можно сказать, что одна вещь похоже будет полезной для управления процессами: можно выполнить определенную операцию над всей группой процессов с помощью одной команды. Следующий пример демонстрирует это, а также ограничение приоритетов внутри групп процессов. Закомментированный цифры в круглых скобках обеспечивают ссылку для сравнения результата работы.
//: c14:ThreadGroup1.java
// How thread groups control priorities
// of the threads inside them.

public class ThreadGroup1 {
  public static void main(String[] args) {
    // Get the system thread & print its Info:
    ThreadGroup sys = 
      Thread.currentThread().getThreadGroup();
    sys.list(); // (1)
    // Reduce the system thread group priority:
    sys.setMaxPriority(Thread.MAX_PRIORITY - 1);
    // Increase the main thread priority:
    Thread curr = Thread.currentThread();
    curr.setPriority(curr.getPriority() + 1);
    sys.list(); // (2)
    // Attempt to set a new group to the max:
    ThreadGroup g1 = new ThreadGroup("g1");
    g1.setMaxPriority(Thread.MAX_PRIORITY);
    // Attempt to set a new thread to the max:
    Thread t = new Thread(g1, "A");
    t.setPriority(Thread.MAX_PRIORITY);
    g1.list(); // (3)
    // Reduce g1's max priority, then attempt
    // to increase it:
    g1.setMaxPriority(Thread.MAX_PRIORITY - 2);
    g1.setMaxPriority(Thread.MAX_PRIORITY);
    g1.list(); // (4)
    // Attempt to set a new thread to the max:
    t = new Thread(g1, "B");
    t.setPriority(Thread.MAX_PRIORITY);
    g1.list(); // (5)
    // Lower the max priority below the default
    // thread priority:
    g1.setMaxPriority(Thread.MIN_PRIORITY + 2);
    // Look at a new thread's priority before
    // and after changing it:
    t = new Thread(g1, "C");
    g1.list(); // (6)
    t.setPriority(t.getPriority() -1);
    g1.list(); // (7)
    // Make g2 a child Threadgroup of g1 and
    // try to increase its priority:
    ThreadGroup g2 = new ThreadGroup(g1, "g2");
    g2.list(); // (8)
    g2.setMaxPriority(Thread.MAX_PRIORITY);
    g2.list(); // (9)
    // Add a bunch of new threads to g2:
    for (int i = 0; i < 5; i++)
      new Thread(g2, Integer.toString(i));
    // Show information about all threadgroups
    // and threads:
    sys.list(); // (10)
    System.out.println("Starting all threads:");
    Thread[] all = new Thread[sys.activeCount()];
    sys.enumerate(all);
    for(int i = 0; i < all.length; i++)
      if(!all[i].isAlive())
        all[i].start();
    // Suspends & Stops all threads in 
    // this group and its subgroups:
    System.out.println("All threads started");
    sys.suspend(); // Deprecated in Java 2
    // Never gets here...
    System.out.println("All threads suspended");
    sys.stop(); // Deprecated in Java 2
    System.out.println("All threads stopped");
  }
} ///:~
Результат работы программы, представленный ниже, был отредактирован, чтобы уместиться на странице (java.lang. удалено), а также добавлены цифры, чтобы ссылаться на закомментированные цифры по тексту программы приведенной выше.
(1) ThreadGroup[name=system,maxpri=10]
      Thread[main,5,system]
(2) ThreadGroup[name=system,maxpri=9]
      Thread[main,6,system]
(3) ThreadGroup[name=g1,maxpri=9]
      Thread[A,9,g1]
(4) ThreadGroup[name=g1,maxpri=8]
      Thread[A,9,g1]
(5) ThreadGroup[name=g1,maxpri=8]
      Thread[A,9,g1]
      Thread[B,8,g1]
(6) ThreadGroup[name=g1,maxpri=3]
      Thread[A,9,g1]
      Thread[B,8,g1]
      Thread[C,6,g1]
(7) ThreadGroup[name=g1,maxpri=3]
      Thread[A,9,g1]
      Thread[B,8,g1]
      Thread[C,3,g1]
(8) ThreadGroup[name=g2,maxpri=3]
(9) ThreadGroup[name=g2,maxpri=3]
(10)ThreadGroup[name=system,maxpri=9]
      Thread[main,6,system]
      ThreadGroup[name=g1,maxpri=3]
        Thread[A,9,g1]
        Thread[B,8,g1]
        Thread[C,3,g1]
        ThreadGroup[name=g2,maxpri=3]
          Thread[0,6,g2]
          Thread[1,6,g2]
          Thread[2,6,g2]
          Thread[3,6,g2]
          Thread[4,6,g2]
Starting all threads:
All threads started
У всех программ есть как минимум один запущенный процесс и первое действие в main() является вызовом static метода для Thread называемого currentThread(). Из этого процесса создается группа процессов и вызывается list() для отображения следующего результата:
(1) ThreadGroup[name=system,maxpri=10]
      Thread[main,5,system]

Можно видеть, что имя основной группы system, а имя основного процесса main и он принадлежит группе процессов system.

Второй пример (exercise) показывает, что максимальный приоритет группы system может быть уменьшен, а процесс main может увеличить свой приоритет:

(2) ThreadGroup[name=system,maxpri=9]
      Thread[main,6,system]
Третий пример создает группу процессов g1, которая автоматически принадлежит системной группе процессов поскольку для нее не установлено что-то иное. Новый процесс А помещается в g1.  После попытки установить наивысшее значение для максимального приоритета этой группы и наивысшее значение для приоритет процесса А результат будет следующий:
(3) ThreadGroup[name=g1,maxpri=9]
      Thread[A,9,g1]

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

Четвертый пример уменьшает максимальное значение приоритета для g1, а затем пытается вернуть его обратно к Thread.MAX_PRIORITY. Результат следующий:

(4) ThreadGroup[name=g1,maxpri=8]
      Thread[A,9,g1]

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

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

(5) ThreadGroup[name=g1,maxpri=8]
      Thread[A,9,g1]
      Thread[B,8,g1]

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

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

(6) ThreadGroup[name=g1,maxpri=3]
      Thread[A,9,g1]
      Thread[B,8,g1]
      Thread[C,6,g1]

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

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

(7) ThreadGroup[name=g1,maxpri=3]
      Thread[A,9,g1]
      Thread[B,8,g1]
      Thread[C,3,g1]

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

Аналогичный эксперимент проводился в (8) и (9), в котором создается новая дочерняя группа процессов g2 от g1, а затем максимальное значение ее приоритета изменяется. И видно, что невозможно установить максимальное значение приоритета для g2 выше, чем у g1:

(8) ThreadGroup[name=g2,maxpri=3]
(9) ThreadGroup[name=g2,maxpri=3]

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

После всех этих экспериментов выводиться полный список всех групп и процессов:

(10)ThreadGroup[name=system,maxpri=9]
      Thread[main,6,system]
      ThreadGroup[name=g1,maxpri=3]
        Thread[A,9,g1]
        Thread[B,8,g1]
        Thread[C,3,g1]
        ThreadGroup[name=g2,maxpri=3]
          Thread[0,6,g2]
          Thread[1,6,g2]
          Thread[2,6,g2]
          Thread[3,6,g2]
          Thread[4,6,g2]

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

Последняя часть данной программы демонстрирует методы для всей группы процессов. В начале программа перемещается по всему дереву процессов и запускает те из них, который еще не запущены. Чтобы все было не безоблачно, системная группа затем временно отключается (suspend) и, в конце концов, останавливается. (Также довольно интересно наблюдать как suspend() и work() работают со всей группой процессов, но помните, что данные методы запрещены (depricated) в Java 2.) Но в тот момент когда вы приостановили группу system, вы также приостанавливаете процесс main и вся программа падает (shut down), и она ни когда не дойдет до той точки, где программа останавливается. В действительности, при попытке остановить процесс main он генерирует исключение ThreadDeath, то есть не типичная ситуация. Поскольку ThreadGroup является наследником Object содержащий метод wait(), то можно также приостановить выполнение программы на какой-то промежуток времени вызовом wait(seconds * 1000). Конечно это должно установить блокировку внутри синхронизированного блока.

Класс ThreadGroup имеет также методы suspend( ) и resume( ), так что можно остановить и запустить всю группу процессов и все процессы и подгруппы в этой группе с помощью простых команд. (И еще раз, suspend( ) и resume( ) запрещены в Java 2.)

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

Повторное изучение Runnable

Ранее в этой главе я советовал, чтобы вы тщательно подумали прежде чем сделать апплет или основной Frame реализацией от Runnable. Конечно, если вы должны наследовать от класса и хотите добавить поведение как у процесса для класса, то Runnable будет правильным решением. Последний пример в этой главе показывает это создав класс RunnableJPanel рисующий различные цвета. Данное приложение сделано так, что принимает различные значения из командной строки чтобы определить размер таблицы цветов и какой промежуток времени sleep() между перерисовкой другим цветом. Играясь с этими параметрами можно обнаружить некоторое интересное и, возможно, необъяснимое поведение процесса:
//: c14:ColorBoxes.java
// Using the Runnable interface.
// <applet code=ColorBoxes width=500 height=400>
// <param name=grid value="12">
// <param name=pause value="50">
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

class CBox extends JPanel implements Runnable {
  private Thread t;
  private int pause;
  private static final Color[] colors = { 
    Color.black, Color.blue, Color.cyan, 
    Color.darkGray, Color.gray, Color.green,
    Color.lightGray, Color.magenta, 
    Color.orange, Color.pink, Color.red, 
    Color.white, Color.yellow 
  };
  private Color cColor = newColor();
  private static final Color newColor() {
    return colors[
      (int)(Math.random() * colors.length)
    ];
  }
  public void paintComponent(Graphics  g) {
    super.paintComponent(g);
    g.setColor(cColor);
    Dimension s = getSize();
    g.fillRect(0, 0, s.width, s.height);
  }
  public CBox(int pause) {
    this.pause = pause;
    t = new Thread(this);
    t.start(); 
  }
  public void run() {
    while(true) {
      cColor = newColor();
      repaint();
      try {
        t.sleep(pause);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
    } 
  }
} 

public class ColorBoxes extends JApplet {
  private boolean isApplet = true;
  private int grid = 12;
  private int pause = 50;
  public void init() {
    // Get parameters from Web page:
    if (isApplet) {
      String gsize = getParameter("grid");
      if(gsize != null)
        grid = Integer.parseInt(gsize);
      String pse = getParameter("pause");
      if(pse != null)
        pause = Integer.parseInt(pse);
    }
    Container cp = getContentPane();
    cp.setLayout(new GridLayout(grid, grid));
    for (int i = 0; i < grid * grid; i++)
      cp.add(new CBox(pause));
  }
  public static void main(String[] args) {
    ColorBoxes applet = new ColorBoxes();
    applet.isApplet = false;
    if(args.length > 0)
      applet.grid = Integer.parseInt(args[0]);
    if(args.length > 1) 
      applet.pause = Integer.parseInt(args[1]);
    Console.run(applet, 500, 400);
  }
} ///:~

ColorBoxes обычный апплет/приложение с init( ) реализующим GUI. Это устанавливает GridLayout так, что он имеет ячейки таблицы (grid) в каждом направлении. Затем, для заполнения таблцы, добавляется соответствующее количество объектов CBox передав значение переменной pause для каждой из них. В методе main() можно видеть как pause и grid имеют значения по умолчанию, которые можно изменить передав их через параметры командной строки, либо изменив параметры апплета.

Вся работа происходит в CBox, который является наследником от JPanel и реализует интерфейс Runnable так, что каждый JPanel может быть также и Thread. Запомните, что когда вы реализуете Runnable вы не создаете объекта Thread, а просто класс, имеющий метод run(). Таким образом, можно явно создать объект Thread и применить объект Runnable в конструкторе, затем вызвать start() (что происходит в конструкторе). В CBox данный процесс называется t.

Обратите внимание на массив color являющейся списком всех цветовых значений в классе Color. Это используется в NewColor для случайного выбора цвета. Текущее значение цвета для ячейки определяется как cColor.

paintComponent() совершенно просто - устанавливается значение цвета для cColor и весь JPanel закрашивается данным цветом.

Бесконечный цикл в run() устанавливает cColor в новое, случайно выбранное значение, а затем вызывает метод repaint() для отображения. Затем процесс sleep() (засыпает) на какое-то время, определенное в командной строке.

Именно из-за того, что данный пример гибок, и каждый элемент JPanel действует как процесс, можно поэксперементировать создавая столько процессов сколько необходимо. (На самом деле существует ограничение на количество выполняемых процессов, которыми JVM может эффективно управлять.)

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

Когда процессов слишком много

В некоторый момент вы увидите, что ColorBoxes совсем увяз в выполнении. На моей машине это возникало примерно после таблицы 10х10. Но почему такое происходит? Вы подозрительны если считаете, что возможно Swing может что-то творить с этим, так что, вот пример, который проверяет данное утверждение путем создания небольшого количества процессов. Следующий исходный код переделан так, чтобы ArrayList реализует Runnable и данный ArrayList содержит номера цветовых блоков и случайным образом выбирает один для обновления. В результате мы имеем гораздо меньше процессов, чем цветовых блоков, так что при увеличении скорости выполнения мы знаем, что это именно из-за гораздо меньшего количества процессов по сравнению с предыдущим примером:
//: c14:ColorBoxes2.java
// Balancing thread use.
// <applet code=ColorBoxes2 width=600 height=500>
// <param name=grid value="12">
// <param name=pause value="50">
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import com.bruceeckel.swing.*;

class CBox2 extends JPanel {
  private static final Color[] colors = { 
    Color.black, Color.blue, Color.cyan, 
    Color.darkGray, Color.gray, Color.green,
    Color.lightGray, Color.magenta, 
    Color.orange, Color.pink, Color.red, 
    Color.white, Color.yellow 
  };
  private Color cColor = newColor();
  private static final Color newColor() {
    return colors[
      (int)(Math.random() * colors.length)
    ];
  }
  void nextColor() {
    cColor = newColor();
    repaint();
  }
  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    g.setColor(cColor);
    Dimension s = getSize();
    g.fillRect(0, 0, s.width, s.height);
  }
}

class CBoxList 
  extends ArrayList implements Runnable {
  private Thread t;
  private int pause;
  public CBoxList(int pause) {
    this.pause = pause;
    t = new Thread(this);
  }
  public void go() { t.start(); }
  public void run() {
    while(true) {
      int i = (int)(Math.random() * size());
      ((CBox2)get(i)).nextColor();
      try {
        t.sleep(pause);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
    } 
  }
  public Object last() { return get(size() - 1);}
}

public class ColorBoxes2 extends JApplet {
  private boolean isApplet = true;
  private int grid = 12;
  // Shorter default pause than ColorBoxes:
  private int pause = 50;
  private CBoxList[] v;
  public void init() {
    // Get parameters from Web page:
    if (isApplet) {
      String gsize = getParameter("grid");
      if(gsize != null)
        grid = Integer.parseInt(gsize);
      String pse = getParameter("pause");
      if(pse != null)
        pause = Integer.parseInt(pse);
    }
    Container cp = getContentPane();
    cp.setLayout(new GridLayout(grid, grid));
    v = new CBoxList[grid];
    for(int i = 0; i < grid; i++)
      v[i] = new CBoxList(pause);
    for (int i = 0; i < grid * grid; i++) {
      v[i % grid].add(new CBox2());
      cp.add((CBox2)v[i % grid].last());
    }
    for(int i = 0; i < grid; i++)
      v[i].go();
  }   
  public static void main(String[] args) {
    ColorBoxes2 applet = new ColorBoxes2();
    applet.isApplet = false;
    if(args.length > 0)
      applet.grid = Integer.parseInt(args[0]);
    if(args.length > 1) 
      applet.pause = Integer.parseInt(args[1]);
    Console.run(applet, 500, 400);
  }
} ///:~

В ColorBoxes2 создается массив CBoxList и инициализируется для хранения grid CBoxList, каждый из которых знает на сколько долго необходимо засыпать. Затем в каждый CBoxList добавляется аналогичное количество объектов CBox2 и  каждый список вызывает go(), что запускает процесс.

CBox2 аналогичен CBox: он рисует себя произвольно выбранным цветом. Но это и все что CBox2 делает. Вся работа с процессами теперь перемещена в CBoxList.

CBoxList также может иметь унаследованный Thread и иметь объект член типа ArrayList. Данное решение имеет преимущества в том, что методам add() и get() затем может быть передан особый аргумент и возвращаемое значение, вместо общих Object'ов. (И их имя также может быть изменено на что-то более кортокое.) На первый взгляд кажется, что приведенном здесь пример требует меньше кодирования. Дополнительно, он автоматически сохраняет все функции выполняемые ArrayList.  Со всеми приведениями и скобками, необходимыми для get() это не будет выходом при росте основного кода программы.

Как и прежде, при реализации Runnable вы не получаете все, что предоставляется вместе с Thread, так что вам необходимо создать новый Thread и самим определить его конструктор, чтобы иметь что-либо для start(), так, как это было сделано в конструкторе CBosList и в go().  Метод run() просто выбирает случайны номер элемента в листе и вызывает nextCollor() для этого элемента, чтобы он применил новый, солучайно выбранный цвет.

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

  1.  Достаточно ли у вас вызовов sleep( ), yield( ), и/или wait( )?
  2.  Насколько продолжителен вызов sleep( )?
  3.  Запустили ли вы слишком много процессов?
  4.  Пробовали ли вы различные платформы и JVM?
Вопросы подобные этим считаются одной из причин, почему программирование с применением множества процессов часто трактуется как искусство.

Резюме

Крайне необходимо выучить, когда необходимо использовать множество процессов и когда можно избежать этого. Основная причина их использования заключается в возможности управления несколькими задачами, смешивание которых приводит к более эффективному использованию компьютера (включая возможность прозрачно распределять задачи между несколькими процессорами) или к удобству пользователя. Классический пример использования ресурсов - это использование процессора во время ожидания операций ввода/вывода. Классический пример удобства для пользователя - это опрос кнопки "stop" во время продолжительного копирования.

Основные же недостатки процессов следующие:

  1.  Падение производительности при ожидании использования общего ресурса
  2.  Требуются дополнительные ресурсы процессора (CPU) для управления процессами
  3.  Непомерная сложность, из-за глупой идеи создать еще один процесса для обновления каждого элемента в массиве
  4.  Патологии, включающие нехватку ресурсов (starving), разность в скорости выполнения (racing) и мертвые блокировки (deadlock)

Дополнительное превосходство процессов в том, что они заменяют "тяжелое" переключение контекста приложения (в пересчете на 100 инструкций) на "легкое" переключение контекста выполнения (в пересчете на 100 инструкций). Поскольку все процессы в данном приложении разделяют одно и то же адресное пространство, то легкое переключение контекста изменяет только выполнение программы и локальные переменные. С другой стороны, изменение приложения - тяжелое переключение контекста - должно поменять всю память.

Использование процессов схоже с шагом в совершенно иной мир и изучение совершенно нового языка программирования, или, по крайне мере, новой концепции языка. С появление поддержки процессов в большинстве операционных систем микрокомпьютеров, расширения для процессов также появились в языках программирования или библиотеках. В любом случае, программирование процессов выглядит мистикой (1)  и требует смещения в способе понимания программирования: и (2) выглядит похожей на реализацию поддержки процессов в других языках программирования, таким образом, если вы поймете процессы, вы поймете и большинство языков. И хотя поддержка процессов может сделать Java похожей на достаточно трудный язык программирования, Java в этом не виновата. Процессы слишком хитрые.

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

В дополнении, приложение с процессами является в какой-то степени искусством. Теоретически Java создавалась для возможности создания такого количества объектов сколько необходимо для решения стоЯщей проблемы. (Хотя создание нескольких миллионов объектов для инженерного анализа конечного множества, например, вряд ли будет практично на Java.) Однако похоже, что существует верхняя граница количеству процессов, которые возможно создать в приложении, поскольку с определнной точки наличие большего количества процессов приложение становится громоздким. Эта критическая точки исчисляется не несколькими тысячами, как это может быть с объектами, а всего лишь парой сотен, иногда даже менее 100. Поскольку в большинстве случаев создается только процессы для решения проблемы, то обычно их количество не превышает установленный лимит, даже в большинстве главных приложений их количество невелико.

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

Стоит обратить внимание на одну вещь, которая пропущена в данной главе, пример с анимацией, что является одной из популярнейших приложений для апплетов. Однако, полное решение этой задачи (со звуком) представлено в Java JDK (доступном на java.sun.com) в разделе примеров. В дополнение можно ожидать лучшей поддержки анимации, что станет частью будущих версий Java, несмотря на то, что появляются полностью различные, не-Java, без необходимости программирования решения для анимации в Web, которые вероятно будут лучше традиционных решений. Для понимания того, как работает анимации в Java можно прочитать Core Java 2 от Horstmann & Cornell, Prentice-Hall, 1997. За дополнительным объяснением процессов смотрите Concurrent Programming in Java написанную Doug Lea, Addison-Wesley, 1997, или Java Threads от Oaks & Wong, O'Reilly, 1997.

Упражнения

Решения отдельных заданий можно посмотреть в электронной книжке The Thinking in Java Annotated Solution Guide, доступную за небольшую плату на сайте www.BruceEckel.com.
  1. Наследуйте класс от Thread и переопределите метод run( ). Внутри run() напечатайте сообщение и вызовите sleep(). Повторите это три раза и выйдете (return)  из run(). Поместите приветственное сообщение в конструктор и переопределите finalaize() чтобы вывести прощальное сообщение. Создайте отдельный вызов процесса, назовите его System.gc() и System.runFinalization() внутри run(), напечатав сообщение, так как они выполняются. Создайте несколько объектов от процессов обоих типов и запустите их чтобы посмотреть, что произойдет.
  2. Измените Sharing2.java добавив блок synchronized внутрь метода run( ) для TwoCounter вместо синхронизации всего run( ) метода.
  3. Создайте два подкласса Thread, один, использующий run( ) для запуска, и перехватывающий ссылку на второй процесс Thread, а затем вызывающий wait( ). Вызов run() второго класса должен вызывать notifyAll( ) для первого процесса после нескольких секунд ожидания, так, чтобы первый процесс при этом вывел сообщение.
  4. В Counter5.java внутри Ticker2, удалите yield( ) и объясните результат работы. Потом замените yield( ) на sleep( ) и объясните этот результат.
  5. В ThreadGroup1.java, замените вызов sys.suspend( ) на вызов wait( ) для группы процессов, установив для них ожидание в две секунды. Для того чтобы это работало корректно необходимо установить блокировку для sys внутри блока synchronized.
  6. Измените Daemons.java так, чтобы main( ) был sleep( ) вместо readLine( ). Поэкспериментируйте с различным значением времени засыпания чтобы увидеть что произойдет.
  7. В Главе 8 найдите пример GreenhouseControls.java, состоящий их трех файлов. В Event.java, класс Event основан на наблюдении времени. Замените Event так, чтобы оно стало процессом Thread, и замените весь пример так, чтобы он работал с новым, основанным на Thread событием Event.
  8. Измените Exercise 7 так, чтобы для запуска системы использовался класс java.util.Timer из JDK 1.3.
  9. Начиная с SineWave.java Главы 13, создайте программу (апплет/приложение используя класс Console) рисующих анимированную синусоиду которая движется аналогично тому, как это происходит в осциллографе, перемещая рисунок с помощью Thread. Скорость перемещения должна управляться с помощью элемента управления java.swing.JSlider.
  10. Измените Exercise 9 так, чтобы было создано несколько звуковых панелей внутри приложения. Количество панелей должно контролироваться либо HTML тэгами, либо параметрами из командной строки.
  11. Измените Exercise 9 так, чтобы класс java.swing.Timer использовался для вывода анимации. Обратите внимание на различия с java.util.Timer.

[70] Runnable было введено в Java 1.0, в то время как внутренние классы появились только в Java 1.1, что необходимо принимать во внимание при объяснение причины существования  Runnable. Также, традиционная архитектура множества процессов сосредоточена на выполняемых функциях, а не объектах. Я предпочитаю всегда создавать наследника от Thread если возможно; для меня это выглядит более понятно и гибче в использовании.

[71]The Java Programming Language, написано Ken Arnold и James Gosling, Addison-Wesley 1996 pp 179.

[ Предыдущая глава ] [ Оглавление ] [ Содержание ] [ Индекс ] [ Следующая глава ]