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

LXF87-88:Java

Материал из Linuxformat
(Различия между версиями)
Перейти к: навигация, поиск
м
(→‎Потоки в Java)
 
(не показаны 11 промежуточных версий 4 участников)
Строка 1: Строка 1:
 +
{{Цикл/Java}}
 +
 
=== Потоки в Java ===
 
=== Потоки в Java ===
−
''ЧАСТЬ 4: Завершая курс молодого Java-бойца, '''Антон Черноусов''' научит вас управлять потоками... Жаль, что не денежными.''
+
''ЧАСТЬ 4: Завершая курс молодого Java-бойца, '''Антон Черноусов''' научит вас управлять потоками… Жаль, что не денежными.''
  
−
C каждым днем появляются все более мощные процессоры, многоядерная архитектура которых стала основной темой ушедшего года, поэтому двухядерный процессор в ноутбуке уже никого не удивляет. С одной стороны – это обстоятельство приближает возможности простого пользователя к возможностям «понастоящему» больших систем. С другой (и рекламные буклеты об этом обычно молчат) – для того, чтобы использовать весь потенциал современных компьютеров, приложение должно «уметь» просчитать задачу фактически на двух или более процессорах.  
+
C каждым днем появляются все более мощные процессоры, многоядерная архитектура которых стала основной темой ушедшего года, поэтому двухядерный процессор в ноутбуке уже никого не удивляет. С одной стороны — это обстоятельство приближает возможности простого пользователя к возможностям «по-настоящему» больших систем. С другой (и рекламные буклеты об этом обычно молчат) — для того, чтобы использовать весь потенциал современных компьютеров, приложение должно «уметь» просчитать задачу фактически на двух или более процессорах.
  
−
Создание эффективных алгоритмов для работы на многопроцессорных станциях – это большая и сложная работа. Несмотря на это, для любого программиста актуальна задача организации взаимодействия с медленными ресурсами (например, чтения, записи или копирования файлов, работы с принтером, сетью), так как немногие пользователи смирятся с тем, что их любимая программа «замирает» в момент выполнения какой-либо операции.
+
Создание эффективных алгоритмов для работы на многопроцессорных станциях — это большая и сложная работа. Несмотря на это, для любого программиста актуальна задача организации взаимодействия с медленными ресурсами (например, чтения, записи или копирования файлов, работы с принтером, сетью), так как немногие пользователи смирятся с тем, что их любимая программа «замирает» в момент выполнения какой-либо операции.
  
−
Во избежание описанных проблем программа должна использовать потоки или процессы. Под процессом понимается заявка на потребле-
+
Во избежание описанных проблем программа должна использовать потоки или процессы. Под процессом понимается заявка на потребление всех видов ресурсов системы, кроме одного — процессорного времени, или иначе говоря, процесс — это запущенная на выполнение программа (такое определение дается в [[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.
  
 
=== Поток, он же thread ===
 
=== Поток, он же thread ===
Строка 43: Строка 44:
 
}
 
}
 
</source>
 
</source>
−
При выполнении метода main() класса ConsoleToThread создается объект-поток FirstThread, который запускается на выполнение методом start() [заметьте – метод run() никогда не вызывается явно, – прим.ред.]. Метод join() используется в случае, когда необходимо «дождаться» завершения потока. Завершение работы потока происходит при выходе из метода run(), как явном (например, посредством return), так и неявном (если внутри метода возникло и не было обработано какое-то исключение).
+
При выполнении метода main() класса ConsoleToThread создается объект-поток FirstThread, который запускается на выполнение методом start() [заметьте — метод run() никогда не вызывается явно, — прим.ред.]. Метод join() используется в случае, когда необходимо «дождаться» завершения потока. Завершение работы потока происходит при выходе из метода run(), как явном (например, посредством return), так и неявном (если внутри метода возникло и не было обработано какое-то исключение).
  
 
Имейте в виду (это важно!): повторный запуск уже отработавшего потока приведет к исключению IllegalThreadStateException.
 
Имейте в виду (это важно!): повторный запуск уже отработавшего потока приведет к исключению IllegalThreadStateException.
  
 
=== Реализация потока через Runnable ===
 
=== Реализация потока через Runnable ===
 +
Давайте теперь рассмотрим пример работы с потоками через интерфейс Runnable. Если, допустим, класс SameRunnable реализует интерфейс Runnable, то запустить поток на основе этого класса на выполнение можно следующим образом:
 +
<source lang = "java">
 +
Runnable run = new SameRunnable();
 +
Thread thread = new Thread(run);
 +
thread.start();
 +
</source>
 +
В следующем примере в методе main() класса ConsoleToThreadTwo создается массив threadArray, состоящий из объектов-потоков. При
 +
этом используется конструктор класса Thread, принимающий два параметра: ссылку на объект, реализующий интерфейс Runnable и
 +
имя потока:
 +
<source lang = "java">
 +
Thread thread = new Thread(Runnable, ThreadName);
 +
</source>
 +
Метод getName() объекта, реализующего поток, возвращает указанное при создании имя. Обратите внимание, что в нем используется
 +
статический метод Thread.currentThread(), возвращающий ссылку на объект Thread, соответствующий выполняющемуся в текущий момент
 +
потоку:
 +
<source lang = "java">
 +
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");
 +
      }
 +
  }
 +
}
 +
</source>
 +
Проследив за выводом этой программы, можно заметить, что процессорное время распределяется между потоками практически равномерно, однако порядок их выполнения во многом случаен.
 +
 
=== Приоритеты потоков ===
 
=== Приоритеты потоков ===
 +
Для управления величиной процессорного времени, выделяемого потоку, можно воспользоваться приоритетами. Установка приоритетов происходит с помощью метода Thread.setPriority(), узнать текущий приоритет позволяет метод getPriority(). В классе Thread определены
 +
три константы:
 +
<source lang = "java">
 +
MIN_PRIORITY = 1
 +
NORM_PRIORITY = 5
 +
MAX_PRIORITY = 10
 +
</source>
 +
Важно понимать, что значение приоритета потока предназначено для Java-машины и не соответствует реальным приоритетам потоков
 +
в операционной системе.
 +
 +
Давайте немного изменим код метода main() класса ConsoleToThreadTwo:
 +
<source lang = "java">
 +
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");
 +
  }
 +
}
 +
</source>
 +
Анализ результатов работы класса показывает, что потоки, получившие более высокий приоритет, выполняются чаще. Также, благодаря
 +
условию на значение переменной pr, setPriority() никогда не будет передан приоритет, превышающий 10 (MAX_PRIORITY). Если бы это про-
 +
изошло, система выбросила бы исключение IllegalArgumentException.
 +
 
=== Потоки-демоны ===
 
=== Потоки-демоны ===
 +
Сделаем еще одно важное замечание: программа будет выполняться до тех пор, пока выполняется хотя бы один запущенный в ней поток;
 +
единственным исключением являются потоки-демоны.
 +
 +
Что же это такое? Демон отличается от «простого смертного» потока вызовом метода setDeamon(true), который необходимо сделать до
 +
начала работы. Например, так:
 +
<source lang = "java">
 +
FirstThread thread = new FirstThread();
 +
thread.setDeamon(true);
 +
thread.start();
 +
</source>
 +
Узнать, является ли поток демоном, можно с помощью метода isDeamon(). Обычно потоки-демоны создаются для обслуживания
 +
некритичных задач, так как при завершении работы программа не дожидается их остановки, а прерывает их самостоятельно.
 +
 
=== Где искать потоки? ===
 
=== Где искать потоки? ===
 +
В большинстве случаев бывает необходимо отслеживать ранее запущенные на выполнение потоки. Использование массива потоков,
 +
как в предыдущем примере, не всегда оправданно. Для хранения и обработки потоков в Java существует класс ThreadGroup. Группа,
 +
к которой принадлежит создаваемый поток, опять-таки передается конструктору Thread():
 +
<source lang = "java">
 +
ThreadGroup tg = new ThreadGroup("NameThreadGroup");
 +
Thread thread = new Thread(tg, new SecondThread(), "ThreadName");
 +
</source>
 +
Если группа не указана явно, поток будет помещен в тот же ThreadGroup, что и его родитель.
 +
<source lang = "java">
 +
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());
 +
}
 +
</source>
 +
В представленном выше примере в экземпляр класса ThreadGroup помещаются три потока, два из которых запускаются на выполнение.
 +
Количество активных потоков в группе определяется с помощью метода activeCount(), а в результате выполнения метода enumerate() формируется перечень всех активных потоков.
 +
 
=== Управление потоками ===
 
=== Управление потоками ===
 +
При запуске потоков следует учитывать и то, что их иногда приходится останавливать, причем как штатно, так и экстренно. Для того, чтобы
 +
приостановить работу потока изнутри, допустим, в тот момент, когда закончилась доступные для обработки данные, можно использовать
 +
два метода: sleep() и wait().
 +
<source lang = "java">
 +
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");
 +
        }
 +
      }
 +
    }
 +
  }
 +
}
 +
</source>
 +
Методу sleep() передается переменная типа long, соответствующая количеству миллисекунд, в течении которых поток будет «спать». В
 +
случае wait() поток ждет пробуждения снаружи. Применение методов sleep() и wait() требует обработки исключительной ситуации, которая возникают при пробуждении потока. В представленном ниже классе ConsoleToThreadThree потоки пробуждаются с помощью метода interrupt():
 +
<source lang = "java">
 +
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");
 +
    }
 +
  }
 +
}
 +
</source>
 +
Отметим, что вызов метода 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-метода представлен ниже:
 +
<source lang = "java">
 +
public synchronized boolean sameCheck() {
 +
  if (a) {
 +
    a = false; return true;
 +
  }
 +
  else {
 +
    a = true;
 +
    return false;
 +
  }
 +
}
 +
</source>
 +
В целом, синхронизация — это механизм, обеспечивающий монопольный доступ участка кода к некоторому объекту. Одним из первых
 +
способов, предложенных для синхронизации работы потоков, были семафоры, концепцию которых описал Дейкстра [Dijkstra] в 1965 году
 +
(часто говорят, что семафор — это классический синхронизированный примитив). Семафор используется для предоставления доступа к огра-
 +
ниченному количеству ресурсов. Как правило, у семафора есть две операции: P — занять ресурс и V — освободить ресурс. Он может быть
 +
реализован следующим образом:
 +
<source lang = "java">
 +
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();
 +
  }
 +
}
 +
</source>
 +
Можно, конечно, реализовывать семафоры самостоятельно, но проще воспользоваться специальной библиотекой java.util.concurrent.
 +
Кроме семафоров, она включает в себя еще много чего интересного.
 +
 +
Отметим, что программа, в принципе, может не использовать ни один из методов синхронизации, обходясь методами wait() и notify().
 +
 
=== Взаимные блокировки ===
 
=== Взаимные блокировки ===
 +
На этом наш рассказ можно было бы и завершить, но чтобы у вас не сложилось впечатление, что в мире многопоточных приложений все
 +
так радужно, мы поговорим о неприятных последствиях синхронизации. Как только количество потоков начинает стремительно расти и
 +
возникает необходимость синхронизированного доступа к ограниченному кругу объектов в различной последовательности, будьте готовы к
 +
ошибкам типа deadlock — взаимным блокировкам.
 +
 +
Взаимная блокировка — это ошибка, которая лучше всего описывается простой формулой: «Поток A держит монитор a и хочет захватить
 +
монитор b, а поток B держит монитор b и хочет захватить монитор a». В результате оба засыпают «мертвым сном».
 +
 +
Ошибка очень противная и возникает обычно в нетривиальных алгоритмах. Лечится взаимная блокировка грамотным проектированием и профилактическими мерами, вроде следующей: всегда захватывайте мониторы в одном и том же порядке.
 +
 +
Сегодня мы поговорили о двух способах создания потоков Java, разобрались с приоритетами, познакомились со средствами управления работой потоков и демонами, а также сделали небольшой обзор методов синхронизации. Для того, чтобы начать практическую работу с потоками, этого вполне достаточно. Желающим разобраться во всем этом глубже я рекомендую ознакомится с книгой «Concurrent Programming in Java: Design Principles and Patterns», автором которой является Дуг Ли [Doug Lea] — она считается одной из лучших по данной тематике.
 +
 +
На этом мы заканчиваем обзор основ программирования на Java и в [[LXF89:Java_EE|следующий раз]] поговорим о серверных приложениях — приготовьтесь
 +
к Java Enterprise Edition!
 +
 
=== Литература ===
 
=== Литература ===
−
*1. П. Кью «Использование UNIX», ISBN 5-8275-0019-4
+
* 1. П. Кью «Использование UNIX», ISBN 5-8275-0019-4
−
*2. В.Г. Олифер, Н.А. Олифер «Сетевые операционные системы», ISBN 5-272-00120-6
+
* 2. В. Г. Олифер, Н. А. Олифер «Сетевые операционные системы», ISBN 5-272-00120-6
−
*3. Д. Бэкон, Т. Харрис «Операционные системы», ISBN 5-94723-969-8
+
* 3. Д. Бэкон, Т. Харрис «Операционные системы», ISBN 5-94723-969-8
−
*4. М. Фаулер «Архитектура корпоративных программных приложений», ISBN 5-8459-0579-6
+
* 4. М. Фаулер «Архитектура корпоративных программных приложений», ISBN 5-8459-0579-6

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