Журнал LinuxFormat - перейти на главную

LXF87-88:Java

Материал из Linuxformat
Перейти к: навигация, поиск

Содержание

Потоки в Java

ЧАСТЬ 4: Завершая курс молодого Java-бойца, Антон Черноусов научит вас управлять потоками... Жаль, что не денежными.

C каждым днем появляются все более мощные процессоры, многоядерная архитектура которых стала основной темой ушедшего года, поэтому двухядерный процессор в ноутбуке уже никого не удивляет. С одной стороны – это обстоятельство приближает возможности простого пользователя к возможностям «понастоящему» больших систем. С другой (и рекламные буклеты об этом обычно молчат) – для того, чтобы использовать весь потенциал современных компьютеров, приложение должно «уметь» просчитать задачу фактически на двух или более процессорах.

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

Во избежание описанных проблем программа должна использовать потоки или процессы. Под процессом понимается заявка на потребле- ние всех видов ресурсов системы, кроме одного – процессорного времени, или иначе говоря, процесс – это запущенная на выполнение программа (такое определение дается в [1]). Поток рассматривается как самостоятельная активность внутри процесса, хотя существуют другие трактовки этого понятия, которые зависят от используемой операционной системы (см., например, [2]). Поток получил свое название по аналогии с потоком команд, поступающих в процессор; при выполнении потоки делят адресное пространство и выделенную память внутри одного процесса. Процессорное время распределяется между различными потоками операционной системой, точнее, одним из компонентов ее ядра – планировщиком. Более полно с понятием процессов и потоков и механизмов работы с ними с точки зрения операционной системы вы можете ознакомиться в книге [3].

Давайте завершим наш экскурс в теорию и окунемся в реальность Java. Под процессом здесь принято понимать всеобъемлющий контекст выполнения, обеспечивающий высокий уровень изоляции охватываемых им данных от внешнего мира, а под потоком – более «легковесный» активный агент; в контексте одного процесса может функционировать целое множество потоков [4]. Планирование потоков в Java обеспечивается внутренними механизмами JVM.

Поток, он же thread

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

public class FirstThread extends Thread {
  public void run(){
    for (int i = 1 ; i < 30; i++)
      System.out.println("It is in thread "+ i);
  }
}

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

public class ConsoleToThread {
  public static void main(String[] args) {
    FirstThread thread = new FirstThread();
    thread.start();
    for (int i = 1; i < 20; i++) {
      System.out.println("It is in main " + i);
    }
    try {
      thread.join();
    } 
    catch (InterruptedException ex) {
      System.out.println("Exception in stop thread");
    }
  }
}

При выполнении метода main() класса ConsoleToThread создается объект-поток FirstThread, который запускается на выполнение методом start() [заметьте – метод run() никогда не вызывается явно, – прим.ред.]. Метод join() используется в случае, когда необходимо «дождаться» завершения потока. Завершение работы потока происходит при выходе из метода run(), как явном (например, посредством return), так и неявном (если внутри метода возникло и не было обработано какое-то исключение).

Имейте в виду (это важно!): повторный запуск уже отработавшего потока приведет к исключению IllegalThreadStateException.

Реализация потока через Runnable

Давайте теперь рассмотрим пример работы с потоками через интерфейс Runnable. Если, допустим, класс SameRunnable реализует интерфейс Runnable, то запустить поток на основе этого класса на выполнение можно следующим образом:

Runnable run = new SameRunnable();
Thread thread = new Thread(run);
thread.start();

В следующем примере в методе main() класса ConsoleToThreadTwo создается массив threadArray, состоящий из объектов-потоков. При этом используется конструктор класса Thread, принимающий два параметра: ссылку на объект, реализующий интерфейс Runnable и имя потока:

Thread thread = new Thread(Runnable, ThreadName);

Метод getName() объекта, реализующего поток, возвращает указанное при создании имя. Обратите внимание, что в нем используется статический метод Thread.currentThread(), возвращающий ссылку на объект Thread, соответствующий выполняющемуся в текущий момент потоку:

public class SecondThread implements Runnable {
  public String getName() {
    return Thread.currentThread().getName();
  }
  public void run() {
    for (int i = 1; i < 100000; i++) {
      if ((i % 10000) == 0) {
        System.out.println(getName() + " counts " + i / 10000);
      }
    }
  }
}
 
public class ConsoleToThreadTwo {
  public static void main(String[] args) {
    Thread[] threadArray = new Thread[3];
      for (int i=0; i<threadArray.length; i++){
        threadArray[i] = new Thread(new SecondThread(), "Thread " + i);
      }
      for (int i=0; i<threadArray.length; i++){
        threadArray[i].start();
        System.out.println(threadArray[i].getName() + " started");
      }
  }
}

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

Приоритеты потоков

Для управления величиной процессорного времени, выделяемого потоку, можно воспользоваться приоритетами. Установка приоритетов происходит с помощью метода Thread.setPriority(), узнать текущий приоритет позволяет метод getPriority(). В классе Thread определены три константы:

MIN_PRIORITY = 1
NORM_PRIORITY = 5
MAX_PRIORITY = 10

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

Давайте немного изменим код метода main() класса ConsoleToThreadTwo:

public static void main(String[] args) {
  Thread[] threadArray = new Thread[3];
  int pr = 0;
  for (int i=0; i<threadArray.length; i++){
    threadArray[i] = new Thread(new SecondThread(), "Thread " + i);
    if (pr == 10) 
      pr = 0; 
    threadArray[i].setPriority(Thread.MIN_PRIORITY + pr);
    pr++;
  }
  for (int i=0; i<threadArray.length; i++){
    threadArray[i].start();
    System.out.println(threadArray[i].getName() + " started");
  }
}

Анализ результатов работы класса показывает, что потоки, получившие более высокий приоритет, выполняются чаще. Также, благодаря условию на значение переменной pr, setPriority() никогда не будет передан приоритет, превышающий 10 (MAX_PRIORITY). Если бы это про- изошло, система выбросила бы исключение IllegalArgumentException.

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

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

Что же это такое? Демон отличается от «простого смертного» потока вызовом метода setDeamon(true), который необходимо сделать до начала работы. Например, так:

FirstThread thread = new FirstThread();
thread.setDeamon(true);
thread.start();

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

Где искать потоки?

В большинстве случаев бывает необходимо отслеживать ранее запущенные на выполнение потоки. Использование массива потоков, как в предыдущем примере, не всегда оправданно. Для хранения и обработки потоков в Java существует класс ThreadGroup. Группа, к которой принадлежит создаваемый поток, опять-таки передается конструктору Thread():

ThreadGroup tg = new ThreadGroup("NameThreadGroup");
Thread thread = new Thread(tg, new SecondThread(), "ThreadName");

Если группа не указана явно, поток будет помещен в тот же ThreadGroup, что и его родитель.

ThreadGroup tg = new ThreadGroup("NameThreadGroup");
Thread thread = new Thread(tg, new SecondThread(), "ThreadName");
Thread thread1 = new Thread(tg, new SecondThread(), "ThreadName2");
Thread thread2 = new Thread(tg, new SecondThread(), "ThreadName3");
System.out.println("active thread in group " + tg.activeCount());
thread.start();
thread1.start();
System.out.println("active thread in group " + tg.activeCount());
Thread[] threads = new Thread[tg.activeCount()];
int m = tg.enumerate(threads);
System.out.println("taked threads from group : " + m);
for (int i = 0; i < threads.length; i++) {
  System.out.println(threads[i].getName());
}

В представленном выше примере в экземпляр класса ThreadGroup помещаются три потока, два из которых запускаются на выполнение. Количество активных потоков в группе определяется с помощью метода activeCount(), а в результате выполнения метода enumerate() формируется перечень всех активных потоков.

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

При запуске потоков следует учитывать и то, что их иногда приходится останавливать, причем как штатно, так и экстренно. Для того, чтобы приостановить работу потока изнутри, допустим, в тот момент, когда закончилась доступные для обработки данные, можно использовать два метода: sleep() и wait().

public class ThirdThread extends Thread {
  public void run() {
    for (int i = 1; i < 110; i++) {
      if (i == 10) {
        try {
          sleep(10000);
        } 
        catch (InterruptedException e) {
          System.out.println("the thread was awaken there (just a moment ago)");
        }
      }
      if (i == 100) {
        try {
          synchronized (this) { wait();}
        }
        catch (InterruptedException e) {
          System.out.println("the thread was awaken there (just a moment ago)again");
        }
      } 
    }
  }
}

Методу sleep() передается переменная типа long, соответствующая количеству миллисекунд, в течении которых поток будет «спать». В случае wait() поток ждет пробуждения снаружи. Применение методов sleep() и wait() требует обработки исключительной ситуации, которая возникают при пробуждении потока. В представленном ниже классе ConsoleToThreadThree потоки пробуждаются с помощью метода interrupt():

public class ConsoleToThreadThree {
  public static void main(String[] args) {
    ThirdThread thread = new ThirdThread();
    thread.start();
    for (int i = 1; i < 20; i++) {
      System.out.println("It is in main " + i);
    }
    thread.interrupt();
    for (int i = 1; i < 20; i++) {
      System.out.println("It is in main " + i);
    }
    thread.interrupt();
    try {
      thread.join();
    } 
    catch (InterruptedException ex) {
      System.out.println("Exception in stop thread");
    }
  }
}

Отметим, что вызов метода wait() без блока синхронизации (synchronized), о котором мы поговорим чуть ниже, приводит к исключению IllegalMonitorStateException. Возникшая ошибка свидетельствует об отсутствии монитора у объекта (понятие монитора и синхронизация тесно связаны, о чем мы тоже поговорим ниже). Если для приостановления потока был применен метод wait(), то для «пробуждения» потока можно воспользоваться методом notify() или notifyAll(). Первый пробуждает один случайно выбранный спящий поток, а второй пытается пробудить их всех.

Кроме рассмотренных выше методов, иногда бывает целесообразно использовать метод yield(), который приостанавливает работу текущего потока. Метод yield() не переводит поток в режим ожидания, как wait(), но предоставляет другим потокам возможность начать работать раньше, чем допустила бы Java-машина [фактически, поток, вызвавший yield() добровольно отдает свой квант процессорного времени, – прим. ред.]. Метод yield() статичный, так что прекратить с его помощью работу другого потока не получится.

Методов остановки потоков тоже нет (ранее присутствовали методы stop(), resume(), suspend(), но сейчас они объявлены как «deprecated» – то есть нерекомендованными к использованию). На сегодня в Java принят уведомительный стиль остановки потока с помощью пары методов: уже известного нам interrupt(), применяемого снаружи, чтобы выставить флаг завершения и метода isInterrupted(), вызываемого изнутри потока, чтобы узнать состояние флага, свидетельствующего о том, что «пора закругляться».

Мониторы и синхронизация

Взаимные блокировки

Литература

  • 1. П. Кью «Использование UNIX», ISBN 5-8275-0019-4
  • 2. В.Г. Олифер, Н.А. Олифер «Сетевые операционные системы», ISBN 5-272-00120-6
  • 3. Д. Бэкон, Т. Харрис «Операционные системы», ISBN 5-94723-969-8
  • 4. М. Фаулер «Архитектура корпоративных программных приложений», ISBN 5-8459-0579-6
Персональные инструменты
купить
подписаться
Яндекс.Метрика