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

LXF87-88:Java

Материал из Linuxformat
(Различия между версиями)
Перейти к: навигация, поиск
м (Потоки в Java)
(Потоки в Java)
 
Строка 8: Строка 8:
 
Создание эффективных алгоритмов для работы на многопроцессорных станциях — это большая и сложная работа. Несмотря на это, для любого программиста актуальна задача организации взаимодействия с медленными ресурсами (например, чтения, записи или копирования файлов, работы с принтером, сетью), так как немногие пользователи смирятся с тем, что их любимая программа «замирает» в момент выполнения какой-либо операции.
 
Создание эффективных алгоритмов для работы на многопроцессорных станциях — это большая и сложная работа. Несмотря на это, для любого программиста актуальна задача организации взаимодействия с медленными ресурсами (например, чтения, записи или копирования файлов, работы с принтером, сетью), так как немногие пользователи смирятся с тем, что их любимая программа «замирает» в момент выполнения какой-либо операции.
  
Во избежание описанных проблем программа должна использовать потоки или процессы. Под процессом понимается заявка на потребле-
+
Во избежание описанных проблем программа должна использовать потоки или процессы. Под процессом понимается заявка на потребление всех видов ресурсов системы, кроме одного — процессорного времени, или иначе говоря, процесс — это запущенная на выполнение программа (такое определение дается в [[LXF87/88:Java#Литература|[1]]]). Поток рассматривается как самостоятельная активность внутри процесса, хотя существуют другие трактовки этого понятия, которые зависят от используемой операционной системы (см., например, [[LXF87/88:Java#Литература|[2]]]). Поток получил свое название по аналогии с потоком команд, поступающих в процессор; при выполнении потоки делят адресное пространство и выделенную память внутри одного процесса. Процессорное время распределяется между различными потоками операционной системой, точнее, одним из компонентов ее ядра — планировщиком. Более полно с понятием процессов и потоков и механизмов работы с ними с точки зрения операционной системы вы можете ознакомиться в книге [[LXF87/88:Java#Литература|[3]]].
ние всех видов ресурсов системы, кроме одного — процессорного времени, или иначе говоря, процесс — это запущенная на выполнение программа (такое определение дается в [[LXF87/88:Java#Литература|[1]]]). Поток рассматривается как самостоятельная активность внутри процесса, хотя существуют другие трактовки этого понятия, которые зависят от используемой операционной системы (см., например, [[LXF87/88:Java#Литература|[2]]]). Поток получил свое название по аналогии с потоком команд, поступающих в процессор; при выполнении потоки делят адресное пространство и выделенную память внутри одного процесса. Процессорное время распределяется между различными потоками операционной системой, точнее, одним из компонентов ее ядра — планировщиком. Более полно с понятием процессов и потоков и механизмов работы с ними с точки зрения операционной системы вы можете ознакомиться в книге [[LXF87/88:Java#Литература|[3]]].
+
  
 
Давайте завершим наш экскурс в теорию и окунемся в реальность Java. Под процессом здесь принято понимать всеобъемлющий контекст выполнения, обеспечивающий высокий уровень изоляции охватываемых им данных от внешнего мира, а под потоком — более «легковесный» активный агент; в контексте одного процесса может функционировать целое множество потоков [[LXF87/88:Java#Литература|[4]]]. Планирование потоков в Java обеспечивается внутренними механизмами JVM.
 
Давайте завершим наш экскурс в теорию и окунемся в реальность Java. Под процессом здесь принято понимать всеобъемлющий контекст выполнения, обеспечивающий высокий уровень изоляции охватываемых им данных от внешнего мира, а под потоком — более «легковесный» активный агент; в контексте одного процесса может функционировать целое множество потоков [[LXF87/88:Java#Литература|[4]]]. Планирование потоков в Java обеспечивается внутренними механизмами JVM.

Текущая версия на 18:39, 13 мая 2009

Содержание

[править] Потоки в 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(), вызываемого изнутри потока, чтобы узнать состояние флага, свидетельствующего о том, что «пора закругляться».

[править] Мониторы и синхронизация

Что такое «монитор», о котором говорилось выше? Нет, это не дисплей, это — объект, используемый как защелка, то есть в данный момент времени владеть монитором может только один поток. В случае, если поток завладел монитором, говорят, что он «вошел» в монитор, а все остальные потоки, пытающиеся это сделать, будут заморожены (часто говорят, что они «ждут» монитора) до тех пор, пока владелец монитора его не освободит, то есть не покинет.

Если перейти к реалиям Java, то объектов типа монитор в явном виде просто нет! С каждым объектом связан неявный монитор, и чтобы завладеть им, необходимо вызвать метод или блок, помеченный ключевым словом synchronized. Как только поток входит в такой блок, он завладевает монитором объекта, переданного synchronized в качестве параметра. Так происходит и в классе ThirdThread, однако, в момент вызова метода wait(), монитор отпускается. Пример synchronized-метода представлен ниже:

public synchronized boolean sameCheck() {
  if (a) {
    a = false; return true;
  } 
  else {
    a = true; 
    return false;
  }
}

В целом, синхронизация — это механизм, обеспечивающий монопольный доступ участка кода к некоторому объекту. Одним из первых способов, предложенных для синхронизации работы потоков, были семафоры, концепцию которых описал Дейкстра [Dijkstra] в 1965 году (часто говорят, что семафор — это классический синхронизированный примитив). Семафор используется для предоставления доступа к огра- ниченному количеству ресурсов. Как правило, у семафора есть две операции: P — занять ресурс и V — освободить ресурс. Он может быть реализован следующим образом:

public class SimpleSemaphore {
  int counter;
  public SimpleSemaphore() {
    this.counter = 1;
  }
 
  public synchronized void p() throws InterruptedException {
    while (counter == 0) {
      wait();
    }
    counter = counter + 1;
  }
 
  public synchronized void v() throws InterruptedException {
    counter = counter - 1;
    notify();
  }
}

Можно, конечно, реализовывать семафоры самостоятельно, но проще воспользоваться специальной библиотекой java.util.concurrent. Кроме семафоров, она включает в себя еще много чего интересного.

Отметим, что программа, в принципе, может не использовать ни один из методов синхронизации, обходясь методами wait() и notify().

[править] Взаимные блокировки

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

Взаимная блокировка — это ошибка, которая лучше всего описывается простой формулой: «Поток A держит монитор a и хочет захватить монитор b, а поток B держит монитор b и хочет захватить монитор a». В результате оба засыпают «мертвым сном».

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

Сегодня мы поговорили о двух способах создания потоков Java, разобрались с приоритетами, познакомились со средствами управления работой потоков и демонами, а также сделали небольшой обзор методов синхронизации. Для того, чтобы начать практическую работу с потоками, этого вполне достаточно. Желающим разобраться во всем этом глубже я рекомендую ознакомится с книгой «Concurrent Programming in Java: Design Principles and Patterns», автором которой является Дуг Ли [Doug Lea] — она считается одной из лучших по данной тематике.

На этом мы заканчиваем обзор основ программирования на Java и в следующий раз поговорим о серверных приложениях — приготовьтесь к Java Enterprise Edition!

[править] Литература

  • 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
Персональные инструменты
купить
подписаться
Яндекс.Метрика