http://wiki.linuxformat.ru/wiki/api.php?action=feedcontributions&user=Vanuan&feedformat=atomLinuxformat - Вклад участника [ru]2024-03-29T14:44:19ZВклад участникаMediaWiki 1.19.20+dfsg-0+deb7u3http://wiki.linuxformat.ru/wiki/LXF87-88:JavaLXF87-88:Java2009-05-13T15:39:23Z<p>Vanuan: /* Потоки в Java */</p>
<hr />
<div>{{Цикл/Java}}<br />
<br />
=== Потоки в Java ===<br />
''ЧАСТЬ 4: Завершая курс молодого Java-бойца, '''Антон Черноусов''' научит вас управлять потоками… Жаль, что не денежными.''<br />
<br />
C каждым днем появляются все более мощные процессоры, многоядерная архитектура которых стала основной темой ушедшего года, поэтому двухядерный процессор в ноутбуке уже никого не удивляет. С одной стороны — это обстоятельство приближает возможности простого пользователя к возможностям «по-настоящему» больших систем. С другой (и рекламные буклеты об этом обычно молчат) — для того, чтобы использовать весь потенциал современных компьютеров, приложение должно «уметь» просчитать задачу фактически на двух или более процессорах.<br />
<br />
Создание эффективных алгоритмов для работы на многопроцессорных станциях — это большая и сложная работа. Несмотря на это, для любого программиста актуальна задача организации взаимодействия с медленными ресурсами (например, чтения, записи или копирования файлов, работы с принтером, сетью), так как немногие пользователи смирятся с тем, что их любимая программа «замирает» в момент выполнения какой-либо операции.<br />
<br />
Во избежание описанных проблем программа должна использовать потоки или процессы. Под процессом понимается заявка на потребление всех видов ресурсов системы, кроме одного — процессорного времени, или иначе говоря, процесс — это запущенная на выполнение программа (такое определение дается в [[LXF87/88:Java#Литература|[1]]]). Поток рассматривается как самостоятельная активность внутри процесса, хотя существуют другие трактовки этого понятия, которые зависят от используемой операционной системы (см., например, [[LXF87/88:Java#Литература|[2]]]). Поток получил свое название по аналогии с потоком команд, поступающих в процессор; при выполнении потоки делят адресное пространство и выделенную память внутри одного процесса. Процессорное время распределяется между различными потоками операционной системой, точнее, одним из компонентов ее ядра — планировщиком. Более полно с понятием процессов и потоков и механизмов работы с ними с точки зрения операционной системы вы можете ознакомиться в книге [[LXF87/88:Java#Литература|[3]]].<br />
<br />
Давайте завершим наш экскурс в теорию и окунемся в реальность Java. Под процессом здесь принято понимать всеобъемлющий контекст выполнения, обеспечивающий высокий уровень изоляции охватываемых им данных от внешнего мира, а под потоком — более «легковесный» активный агент; в контексте одного процесса может функционировать целое множество потоков [[LXF87/88:Java#Литература|[4]]]. Планирование потоков в Java обеспечивается внутренними механизмами JVM.<br />
<br />
=== Поток, он же thread ===<br />
В Java существует два способа работы с потоками: первый заключается в реализации интерфейса Runnable, второй связан с наследованием<br />
класса Thread, который уже реализует данный интерфейс. В обоих случаях класс должен предоставлять реализацию метода run(). Ниже<br />
приведен пример класса, реализующего поток через наследование класса Thread:<br />
<source lang = "java"><br />
public class FirstThread extends Thread {<br />
public void run(){<br />
for (int i = 1 ; i < 30; i++)<br />
System.out.println("It is in thread "+ i);<br />
}<br />
}<br />
</source><br />
Собственно, метод run() и должен содержать некоторый набор инструкций (разумеется, на языке Java), которые вы хотите выполнить<br />
в отдельном потоке. Например, если вы реализуете функцию копирования файла (а пользователь, скажем, копирует ISO-образ объемом<br />
600 Мб), желательно, чтобы эта операция выполнялась в отдельном потоке, запуск которого можно производить следующим образом:<br />
<source lang = "java"><br />
public class ConsoleToThread {<br />
public static void main(String[] args) {<br />
FirstThread thread = new FirstThread();<br />
thread.start();<br />
for (int i = 1; i < 20; i++) {<br />
System.out.println("It is in main " + i);<br />
}<br />
try {<br />
thread.join();<br />
} <br />
catch (InterruptedException ex) {<br />
System.out.println("Exception in stop thread");<br />
}<br />
}<br />
}<br />
</source><br />
При выполнении метода main() класса ConsoleToThread создается объект-поток FirstThread, который запускается на выполнение методом start() [заметьте — метод run() никогда не вызывается явно, — прим.ред.]. Метод join() используется в случае, когда необходимо «дождаться» завершения потока. Завершение работы потока происходит при выходе из метода run(), как явном (например, посредством return), так и неявном (если внутри метода возникло и не было обработано какое-то исключение).<br />
<br />
Имейте в виду (это важно!): повторный запуск уже отработавшего потока приведет к исключению IllegalThreadStateException.<br />
<br />
=== Реализация потока через Runnable ===<br />
Давайте теперь рассмотрим пример работы с потоками через интерфейс Runnable. Если, допустим, класс SameRunnable реализует интерфейс Runnable, то запустить поток на основе этого класса на выполнение можно следующим образом:<br />
<source lang = "java"><br />
Runnable run = new SameRunnable();<br />
Thread thread = new Thread(run);<br />
thread.start();<br />
</source><br />
В следующем примере в методе main() класса ConsoleToThreadTwo создается массив threadArray, состоящий из объектов-потоков. При<br />
этом используется конструктор класса Thread, принимающий два параметра: ссылку на объект, реализующий интерфейс Runnable и<br />
имя потока:<br />
<source lang = "java"><br />
Thread thread = new Thread(Runnable, ThreadName);<br />
</source><br />
Метод getName() объекта, реализующего поток, возвращает указанное при создании имя. Обратите внимание, что в нем используется<br />
статический метод Thread.currentThread(), возвращающий ссылку на объект Thread, соответствующий выполняющемуся в текущий момент<br />
потоку:<br />
<source lang = "java"><br />
public class SecondThread implements Runnable {<br />
public String getName() {<br />
return Thread.currentThread().getName();<br />
}<br />
public void run() {<br />
for (int i = 1; i < 100000; i++) {<br />
if ((i % 10000) == 0) {<br />
System.out.println(getName() + " counts " + i / 10000);<br />
}<br />
}<br />
}<br />
}<br />
<br />
public class ConsoleToThreadTwo {<br />
public static void main(String[] args) {<br />
Thread[] threadArray = new Thread[3];<br />
for (int i=0; i<threadArray.length; i++){<br />
threadArray[i] = new Thread(new SecondThread(), "Thread " + i);<br />
}<br />
for (int i=0; i<threadArray.length; i++){<br />
threadArray[i].start();<br />
System.out.println(threadArray[i].getName() + " started");<br />
}<br />
}<br />
}<br />
</source><br />
Проследив за выводом этой программы, можно заметить, что процессорное время распределяется между потоками практически равномерно, однако порядок их выполнения во многом случаен.<br />
<br />
=== Приоритеты потоков ===<br />
Для управления величиной процессорного времени, выделяемого потоку, можно воспользоваться приоритетами. Установка приоритетов происходит с помощью метода Thread.setPriority(), узнать текущий приоритет позволяет метод getPriority(). В классе Thread определены<br />
три константы:<br />
<source lang = "java"><br />
MIN_PRIORITY = 1<br />
NORM_PRIORITY = 5<br />
MAX_PRIORITY = 10<br />
</source><br />
Важно понимать, что значение приоритета потока предназначено для Java-машины и не соответствует реальным приоритетам потоков<br />
в операционной системе.<br />
<br />
Давайте немного изменим код метода main() класса ConsoleToThreadTwo:<br />
<source lang = "java"><br />
public static void main(String[] args) {<br />
Thread[] threadArray = new Thread[3];<br />
int pr = 0;<br />
for (int i=0; i<threadArray.length; i++){<br />
threadArray[i] = new Thread(new SecondThread(), "Thread " + i);<br />
if (pr == 10) <br />
pr = 0; <br />
threadArray[i].setPriority(Thread.MIN_PRIORITY + pr);<br />
pr++;<br />
}<br />
for (int i=0; i<threadArray.length; i++){<br />
threadArray[i].start();<br />
System.out.println(threadArray[i].getName() + " started");<br />
}<br />
}<br />
</source><br />
Анализ результатов работы класса показывает, что потоки, получившие более высокий приоритет, выполняются чаще. Также, благодаря<br />
условию на значение переменной pr, setPriority() никогда не будет передан приоритет, превышающий 10 (MAX_PRIORITY). Если бы это про-<br />
изошло, система выбросила бы исключение IllegalArgumentException.<br />
<br />
=== Потоки-демоны ===<br />
Сделаем еще одно важное замечание: программа будет выполняться до тех пор, пока выполняется хотя бы один запущенный в ней поток;<br />
единственным исключением являются потоки-демоны.<br />
<br />
Что же это такое? Демон отличается от «простого смертного» потока вызовом метода setDeamon(true), который необходимо сделать до<br />
начала работы. Например, так:<br />
<source lang = "java"><br />
FirstThread thread = new FirstThread();<br />
thread.setDeamon(true);<br />
thread.start();<br />
</source><br />
Узнать, является ли поток демоном, можно с помощью метода isDeamon(). Обычно потоки-демоны создаются для обслуживания<br />
некритичных задач, так как при завершении работы программа не дожидается их остановки, а прерывает их самостоятельно.<br />
<br />
=== Где искать потоки? ===<br />
В большинстве случаев бывает необходимо отслеживать ранее запущенные на выполнение потоки. Использование массива потоков,<br />
как в предыдущем примере, не всегда оправданно. Для хранения и обработки потоков в Java существует класс ThreadGroup. Группа,<br />
к которой принадлежит создаваемый поток, опять-таки передается конструктору Thread():<br />
<source lang = "java"><br />
ThreadGroup tg = new ThreadGroup("NameThreadGroup");<br />
Thread thread = new Thread(tg, new SecondThread(), "ThreadName");<br />
</source><br />
Если группа не указана явно, поток будет помещен в тот же ThreadGroup, что и его родитель.<br />
<source lang = "java"><br />
ThreadGroup tg = new ThreadGroup("NameThreadGroup");<br />
Thread thread = new Thread(tg, new SecondThread(), "ThreadName");<br />
Thread thread1 = new Thread(tg, new SecondThread(), "ThreadName2");<br />
Thread thread2 = new Thread(tg, new SecondThread(), "ThreadName3");<br />
System.out.println("active thread in group " + tg.activeCount());<br />
thread.start();<br />
thread1.start();<br />
System.out.println("active thread in group " + tg.activeCount());<br />
Thread[] threads = new Thread[tg.activeCount()];<br />
int m = tg.enumerate(threads);<br />
System.out.println("taked threads from group : " + m);<br />
for (int i = 0; i < threads.length; i++) {<br />
System.out.println(threads[i].getName());<br />
}<br />
</source><br />
В представленном выше примере в экземпляр класса ThreadGroup помещаются три потока, два из которых запускаются на выполнение.<br />
Количество активных потоков в группе определяется с помощью метода activeCount(), а в результате выполнения метода enumerate() формируется перечень всех активных потоков.<br />
<br />
=== Управление потоками ===<br />
При запуске потоков следует учитывать и то, что их иногда приходится останавливать, причем как штатно, так и экстренно. Для того, чтобы<br />
приостановить работу потока изнутри, допустим, в тот момент, когда закончилась доступные для обработки данные, можно использовать<br />
два метода: sleep() и wait().<br />
<source lang = "java"><br />
public class ThirdThread extends Thread {<br />
public void run() {<br />
for (int i = 1; i < 110; i++) {<br />
if (i == 10) {<br />
try {<br />
sleep(10000);<br />
} <br />
catch (InterruptedException e) {<br />
System.out.println("the thread was awaken there (just a moment ago)");<br />
}<br />
}<br />
if (i == 100) {<br />
try {<br />
synchronized (this) { wait();}<br />
}<br />
catch (InterruptedException e) {<br />
System.out.println("the thread was awaken there (just a moment ago)again");<br />
}<br />
} <br />
}<br />
}<br />
}<br />
</source><br />
Методу sleep() передается переменная типа long, соответствующая количеству миллисекунд, в течении которых поток будет «спать». В<br />
случае wait() поток ждет пробуждения снаружи. Применение методов sleep() и wait() требует обработки исключительной ситуации, которая возникают при пробуждении потока. В представленном ниже классе ConsoleToThreadThree потоки пробуждаются с помощью метода interrupt():<br />
<source lang = "java"><br />
public class ConsoleToThreadThree {<br />
public static void main(String[] args) {<br />
ThirdThread thread = new ThirdThread();<br />
thread.start();<br />
for (int i = 1; i < 20; i++) {<br />
System.out.println("It is in main " + i);<br />
}<br />
thread.interrupt();<br />
for (int i = 1; i < 20; i++) {<br />
System.out.println("It is in main " + i);<br />
}<br />
thread.interrupt();<br />
try {<br />
thread.join();<br />
} <br />
catch (InterruptedException ex) {<br />
System.out.println("Exception in stop thread");<br />
}<br />
}<br />
}<br />
</source><br />
Отметим, что вызов метода wait() без блока синхронизации (synchronized), о котором мы поговорим чуть ниже, приводит к исключению IllegalMonitorStateException. Возникшая ошибка свидетельствует об отсутствии монитора у объекта (понятие монитора и синхронизация<br />
тесно связаны, о чем мы тоже поговорим ниже). Если для приостановления потока был применен метод wait(), то для «пробуждения» потока<br />
можно воспользоваться методом notify() или notifyAll(). Первый пробуждает один случайно выбранный спящий поток, а второй пытается<br />
пробудить их всех.<br />
<br />
Кроме рассмотренных выше методов, иногда бывает целесообразно использовать метод yield(), который приостанавливает работу текущего потока. Метод yield() не переводит поток в режим ожидания, как wait(), но предоставляет другим потокам возможность начать работать<br />
раньше, чем допустила бы Java-машина [фактически, поток, вызвавший yield() добровольно отдает свой квант процессорного времени, -<br />
прим. ред.]. Метод yield() статичный, так что прекратить с его помощью работу другого потока не получится.<br />
<br />
Методов остановки потоков тоже нет (ранее присутствовали методы stop(), resume(), suspend(), но сейчас они объявлены как «deprecated» -<br />
то есть нерекомендованными к использованию). На сегодня в Java принят уведомительный стиль остановки потока с помощью пары методов:<br />
уже известного нам interrupt(), применяемого снаружи, чтобы выставить флаг завершения и метода isInterrupted(), вызываемого изнутри<br />
потока, чтобы узнать состояние флага, свидетельствующего о том, что «пора закругляться».<br />
<br />
=== Мониторы и синхронизация ===<br />
Что такое «монитор», о котором говорилось выше? Нет, это не дисплей, это — объект, используемый как защелка, то есть в данный момент<br />
времени владеть монитором может только один поток. В случае, если поток завладел монитором, говорят, что он «вошел» в монитор, а все<br />
остальные потоки, пытающиеся это сделать, будут заморожены (часто говорят, что они «ждут» монитора) до тех пор, пока владелец монитора<br />
его не освободит, то есть не покинет.<br />
<br />
Если перейти к реалиям Java, то объектов типа монитор в явном виде просто нет! С каждым объектом связан неявный монитор, и чтобы<br />
завладеть им, необходимо вызвать метод или блок, помеченный ключевым словом synchronized. Как только поток входит в такой блок, он<br />
завладевает монитором объекта, переданного synchronized в качестве параметра. Так происходит и в классе ThirdThread, однако, в момент<br />
вызова метода wait(), монитор отпускается. Пример synchronized-метода представлен ниже:<br />
<source lang = "java"><br />
public synchronized boolean sameCheck() {<br />
if (a) {<br />
a = false; return true;<br />
} <br />
else {<br />
a = true; <br />
return false;<br />
}<br />
}<br />
</source><br />
В целом, синхронизация — это механизм, обеспечивающий монопольный доступ участка кода к некоторому объекту. Одним из первых<br />
способов, предложенных для синхронизации работы потоков, были семафоры, концепцию которых описал Дейкстра [Dijkstra] в 1965 году<br />
(часто говорят, что семафор — это классический синхронизированный примитив). Семафор используется для предоставления доступа к огра-<br />
ниченному количеству ресурсов. Как правило, у семафора есть две операции: P — занять ресурс и V — освободить ресурс. Он может быть<br />
реализован следующим образом:<br />
<source lang = "java"><br />
public class SimpleSemaphore {<br />
int counter;<br />
public SimpleSemaphore() {<br />
this.counter = 1;<br />
}<br />
<br />
public synchronized void p() throws InterruptedException {<br />
while (counter == 0) {<br />
wait();<br />
}<br />
counter = counter + 1;<br />
}<br />
<br />
public synchronized void v() throws InterruptedException {<br />
counter = counter - 1;<br />
notify();<br />
}<br />
}<br />
</source><br />
Можно, конечно, реализовывать семафоры самостоятельно, но проще воспользоваться специальной библиотекой java.util.concurrent.<br />
Кроме семафоров, она включает в себя еще много чего интересного.<br />
<br />
Отметим, что программа, в принципе, может не использовать ни один из методов синхронизации, обходясь методами wait() и notify().<br />
<br />
=== Взаимные блокировки ===<br />
На этом наш рассказ можно было бы и завершить, но чтобы у вас не сложилось впечатление, что в мире многопоточных приложений все<br />
так радужно, мы поговорим о неприятных последствиях синхронизации. Как только количество потоков начинает стремительно расти и<br />
возникает необходимость синхронизированного доступа к ограниченному кругу объектов в различной последовательности, будьте готовы к<br />
ошибкам типа deadlock — взаимным блокировкам.<br />
<br />
Взаимная блокировка — это ошибка, которая лучше всего описывается простой формулой: «Поток A держит монитор a и хочет захватить<br />
монитор b, а поток B держит монитор b и хочет захватить монитор a». В результате оба засыпают «мертвым сном».<br />
<br />
Ошибка очень противная и возникает обычно в нетривиальных алгоритмах. Лечится взаимная блокировка грамотным проектированием и профилактическими мерами, вроде следующей: всегда захватывайте мониторы в одном и том же порядке.<br />
<br />
Сегодня мы поговорили о двух способах создания потоков Java, разобрались с приоритетами, познакомились со средствами управления работой потоков и демонами, а также сделали небольшой обзор методов синхронизации. Для того, чтобы начать практическую работу с потоками, этого вполне достаточно. Желающим разобраться во всем этом глубже я рекомендую ознакомится с книгой «Concurrent Programming in Java: Design Principles and Patterns», автором которой является Дуг Ли [Doug Lea] — она считается одной из лучших по данной тематике.<br />
<br />
На этом мы заканчиваем обзор основ программирования на Java и в [[LXF89:Java_EE|следующий раз]] поговорим о серверных приложениях — приготовьтесь<br />
к Java Enterprise Edition!<br />
<br />
=== Литература ===<br />
* 1. П. Кью «Использование UNIX», ISBN 5-8275-0019-4<br />
* 2. В. Г. Олифер, Н. А. Олифер «Сетевые операционные системы», ISBN 5-272-00120-6<br />
* 3. Д. Бэкон, Т. Харрис «Операционные системы», ISBN 5-94723-969-8<br />
* 4. М. Фаулер «Архитектура корпоративных программных приложений», ISBN 5-8459-0579-6</div>Vanuanhttp://wiki.linuxformat.ru/wiki/LXF87-88:JavaLXF87-88:Java2009-05-13T15:37:39Z<p>Vanuan: /* Потоки в Java */</p>
<hr />
<div>{{Цикл/Java}}<br />
<br />
=== Потоки в Java ===<br />
''ЧАСТЬ 4: Завершая курс молодого Java-бойца, '''Антон Черноусов''' научит вас управлять потоками… Жаль, что не денежными.''<br />
<br />
C каждым днем появляются все более мощные процессоры, многоядерная архитектура которых стала основной темой ушедшего года, поэтому двухядерный процессор в ноутбуке уже никого не удивляет. С одной стороны — это обстоятельство приближает возможности простого пользователя к возможностям «по-настоящему» больших систем. С другой (и рекламные буклеты об этом обычно молчат) — для того, чтобы использовать весь потенциал современных компьютеров, приложение должно «уметь» просчитать задачу фактически на двух или более процессорах.<br />
<br />
Создание эффективных алгоритмов для работы на многопроцессорных станциях — это большая и сложная работа. Несмотря на это, для любого программиста актуальна задача организации взаимодействия с медленными ресурсами (например, чтения, записи или копирования файлов, работы с принтером, сетью), так как немногие пользователи смирятся с тем, что их любимая программа «замирает» в момент выполнения какой-либо операции.<br />
<br />
Во избежание описанных проблем программа должна использовать потоки или процессы. Под процессом понимается заявка на потребле-<br />
ние всех видов ресурсов системы, кроме одного — процессорного времени, или иначе говоря, процесс — это запущенная на выполнение программа (такое определение дается в [[LXF87/88:Java#Литература|[1]]]). Поток рассматривается как самостоятельная активность внутри процесса, хотя существуют другие трактовки этого понятия, которые зависят от используемой операционной системы (см., например, [[LXF87/88:Java#Литература|[2]]]). Поток получил свое название по аналогии с потоком команд, поступающих в процессор; при выполнении потоки делят адресное пространство и выделенную память внутри одного процесса. Процессорное время распределяется между различными потоками операционной системой, точнее, одним из компонентов ее ядра — планировщиком. Более полно с понятием процессов и потоков и механизмов работы с ними с точки зрения операционной системы вы можете ознакомиться в книге [[LXF87/88:Java#Литература|[3]]].<br />
<br />
Давайте завершим наш экскурс в теорию и окунемся в реальность Java. Под процессом здесь принято понимать всеобъемлющий контекст выполнения, обеспечивающий высокий уровень изоляции охватываемых им данных от внешнего мира, а под потоком — более «легковесный» активный агент; в контексте одного процесса может функционировать целое множество потоков [[LXF87/88:Java#Литература|[4]]]. Планирование потоков в Java обеспечивается внутренними механизмами JVM.<br />
<br />
=== Поток, он же thread ===<br />
В Java существует два способа работы с потоками: первый заключается в реализации интерфейса Runnable, второй связан с наследованием<br />
класса Thread, который уже реализует данный интерфейс. В обоих случаях класс должен предоставлять реализацию метода run(). Ниже<br />
приведен пример класса, реализующего поток через наследование класса Thread:<br />
<source lang = "java"><br />
public class FirstThread extends Thread {<br />
public void run(){<br />
for (int i = 1 ; i < 30; i++)<br />
System.out.println("It is in thread "+ i);<br />
}<br />
}<br />
</source><br />
Собственно, метод run() и должен содержать некоторый набор инструкций (разумеется, на языке Java), которые вы хотите выполнить<br />
в отдельном потоке. Например, если вы реализуете функцию копирования файла (а пользователь, скажем, копирует ISO-образ объемом<br />
600 Мб), желательно, чтобы эта операция выполнялась в отдельном потоке, запуск которого можно производить следующим образом:<br />
<source lang = "java"><br />
public class ConsoleToThread {<br />
public static void main(String[] args) {<br />
FirstThread thread = new FirstThread();<br />
thread.start();<br />
for (int i = 1; i < 20; i++) {<br />
System.out.println("It is in main " + i);<br />
}<br />
try {<br />
thread.join();<br />
} <br />
catch (InterruptedException ex) {<br />
System.out.println("Exception in stop thread");<br />
}<br />
}<br />
}<br />
</source><br />
При выполнении метода main() класса ConsoleToThread создается объект-поток FirstThread, который запускается на выполнение методом start() [заметьте — метод run() никогда не вызывается явно, — прим.ред.]. Метод join() используется в случае, когда необходимо «дождаться» завершения потока. Завершение работы потока происходит при выходе из метода run(), как явном (например, посредством return), так и неявном (если внутри метода возникло и не было обработано какое-то исключение).<br />
<br />
Имейте в виду (это важно!): повторный запуск уже отработавшего потока приведет к исключению IllegalThreadStateException.<br />
<br />
=== Реализация потока через Runnable ===<br />
Давайте теперь рассмотрим пример работы с потоками через интерфейс Runnable. Если, допустим, класс SameRunnable реализует интерфейс Runnable, то запустить поток на основе этого класса на выполнение можно следующим образом:<br />
<source lang = "java"><br />
Runnable run = new SameRunnable();<br />
Thread thread = new Thread(run);<br />
thread.start();<br />
</source><br />
В следующем примере в методе main() класса ConsoleToThreadTwo создается массив threadArray, состоящий из объектов-потоков. При<br />
этом используется конструктор класса Thread, принимающий два параметра: ссылку на объект, реализующий интерфейс Runnable и<br />
имя потока:<br />
<source lang = "java"><br />
Thread thread = new Thread(Runnable, ThreadName);<br />
</source><br />
Метод getName() объекта, реализующего поток, возвращает указанное при создании имя. Обратите внимание, что в нем используется<br />
статический метод Thread.currentThread(), возвращающий ссылку на объект Thread, соответствующий выполняющемуся в текущий момент<br />
потоку:<br />
<source lang = "java"><br />
public class SecondThread implements Runnable {<br />
public String getName() {<br />
return Thread.currentThread().getName();<br />
}<br />
public void run() {<br />
for (int i = 1; i < 100000; i++) {<br />
if ((i % 10000) == 0) {<br />
System.out.println(getName() + " counts " + i / 10000);<br />
}<br />
}<br />
}<br />
}<br />
<br />
public class ConsoleToThreadTwo {<br />
public static void main(String[] args) {<br />
Thread[] threadArray = new Thread[3];<br />
for (int i=0; i<threadArray.length; i++){<br />
threadArray[i] = new Thread(new SecondThread(), "Thread " + i);<br />
}<br />
for (int i=0; i<threadArray.length; i++){<br />
threadArray[i].start();<br />
System.out.println(threadArray[i].getName() + " started");<br />
}<br />
}<br />
}<br />
</source><br />
Проследив за выводом этой программы, можно заметить, что процессорное время распределяется между потоками практически равномерно, однако порядок их выполнения во многом случаен.<br />
<br />
=== Приоритеты потоков ===<br />
Для управления величиной процессорного времени, выделяемого потоку, можно воспользоваться приоритетами. Установка приоритетов происходит с помощью метода Thread.setPriority(), узнать текущий приоритет позволяет метод getPriority(). В классе Thread определены<br />
три константы:<br />
<source lang = "java"><br />
MIN_PRIORITY = 1<br />
NORM_PRIORITY = 5<br />
MAX_PRIORITY = 10<br />
</source><br />
Важно понимать, что значение приоритета потока предназначено для Java-машины и не соответствует реальным приоритетам потоков<br />
в операционной системе.<br />
<br />
Давайте немного изменим код метода main() класса ConsoleToThreadTwo:<br />
<source lang = "java"><br />
public static void main(String[] args) {<br />
Thread[] threadArray = new Thread[3];<br />
int pr = 0;<br />
for (int i=0; i<threadArray.length; i++){<br />
threadArray[i] = new Thread(new SecondThread(), "Thread " + i);<br />
if (pr == 10) <br />
pr = 0; <br />
threadArray[i].setPriority(Thread.MIN_PRIORITY + pr);<br />
pr++;<br />
}<br />
for (int i=0; i<threadArray.length; i++){<br />
threadArray[i].start();<br />
System.out.println(threadArray[i].getName() + " started");<br />
}<br />
}<br />
</source><br />
Анализ результатов работы класса показывает, что потоки, получившие более высокий приоритет, выполняются чаще. Также, благодаря<br />
условию на значение переменной pr, setPriority() никогда не будет передан приоритет, превышающий 10 (MAX_PRIORITY). Если бы это про-<br />
изошло, система выбросила бы исключение IllegalArgumentException.<br />
<br />
=== Потоки-демоны ===<br />
Сделаем еще одно важное замечание: программа будет выполняться до тех пор, пока выполняется хотя бы один запущенный в ней поток;<br />
единственным исключением являются потоки-демоны.<br />
<br />
Что же это такое? Демон отличается от «простого смертного» потока вызовом метода setDeamon(true), который необходимо сделать до<br />
начала работы. Например, так:<br />
<source lang = "java"><br />
FirstThread thread = new FirstThread();<br />
thread.setDeamon(true);<br />
thread.start();<br />
</source><br />
Узнать, является ли поток демоном, можно с помощью метода isDeamon(). Обычно потоки-демоны создаются для обслуживания<br />
некритичных задач, так как при завершении работы программа не дожидается их остановки, а прерывает их самостоятельно.<br />
<br />
=== Где искать потоки? ===<br />
В большинстве случаев бывает необходимо отслеживать ранее запущенные на выполнение потоки. Использование массива потоков,<br />
как в предыдущем примере, не всегда оправданно. Для хранения и обработки потоков в Java существует класс ThreadGroup. Группа,<br />
к которой принадлежит создаваемый поток, опять-таки передается конструктору Thread():<br />
<source lang = "java"><br />
ThreadGroup tg = new ThreadGroup("NameThreadGroup");<br />
Thread thread = new Thread(tg, new SecondThread(), "ThreadName");<br />
</source><br />
Если группа не указана явно, поток будет помещен в тот же ThreadGroup, что и его родитель.<br />
<source lang = "java"><br />
ThreadGroup tg = new ThreadGroup("NameThreadGroup");<br />
Thread thread = new Thread(tg, new SecondThread(), "ThreadName");<br />
Thread thread1 = new Thread(tg, new SecondThread(), "ThreadName2");<br />
Thread thread2 = new Thread(tg, new SecondThread(), "ThreadName3");<br />
System.out.println("active thread in group " + tg.activeCount());<br />
thread.start();<br />
thread1.start();<br />
System.out.println("active thread in group " + tg.activeCount());<br />
Thread[] threads = new Thread[tg.activeCount()];<br />
int m = tg.enumerate(threads);<br />
System.out.println("taked threads from group : " + m);<br />
for (int i = 0; i < threads.length; i++) {<br />
System.out.println(threads[i].getName());<br />
}<br />
</source><br />
В представленном выше примере в экземпляр класса ThreadGroup помещаются три потока, два из которых запускаются на выполнение.<br />
Количество активных потоков в группе определяется с помощью метода activeCount(), а в результате выполнения метода enumerate() формируется перечень всех активных потоков.<br />
<br />
=== Управление потоками ===<br />
При запуске потоков следует учитывать и то, что их иногда приходится останавливать, причем как штатно, так и экстренно. Для того, чтобы<br />
приостановить работу потока изнутри, допустим, в тот момент, когда закончилась доступные для обработки данные, можно использовать<br />
два метода: sleep() и wait().<br />
<source lang = "java"><br />
public class ThirdThread extends Thread {<br />
public void run() {<br />
for (int i = 1; i < 110; i++) {<br />
if (i == 10) {<br />
try {<br />
sleep(10000);<br />
} <br />
catch (InterruptedException e) {<br />
System.out.println("the thread was awaken there (just a moment ago)");<br />
}<br />
}<br />
if (i == 100) {<br />
try {<br />
synchronized (this) { wait();}<br />
}<br />
catch (InterruptedException e) {<br />
System.out.println("the thread was awaken there (just a moment ago)again");<br />
}<br />
} <br />
}<br />
}<br />
}<br />
</source><br />
Методу sleep() передается переменная типа long, соответствующая количеству миллисекунд, в течении которых поток будет «спать». В<br />
случае wait() поток ждет пробуждения снаружи. Применение методов sleep() и wait() требует обработки исключительной ситуации, которая возникают при пробуждении потока. В представленном ниже классе ConsoleToThreadThree потоки пробуждаются с помощью метода interrupt():<br />
<source lang = "java"><br />
public class ConsoleToThreadThree {<br />
public static void main(String[] args) {<br />
ThirdThread thread = new ThirdThread();<br />
thread.start();<br />
for (int i = 1; i < 20; i++) {<br />
System.out.println("It is in main " + i);<br />
}<br />
thread.interrupt();<br />
for (int i = 1; i < 20; i++) {<br />
System.out.println("It is in main " + i);<br />
}<br />
thread.interrupt();<br />
try {<br />
thread.join();<br />
} <br />
catch (InterruptedException ex) {<br />
System.out.println("Exception in stop thread");<br />
}<br />
}<br />
}<br />
</source><br />
Отметим, что вызов метода wait() без блока синхронизации (synchronized), о котором мы поговорим чуть ниже, приводит к исключению IllegalMonitorStateException. Возникшая ошибка свидетельствует об отсутствии монитора у объекта (понятие монитора и синхронизация<br />
тесно связаны, о чем мы тоже поговорим ниже). Если для приостановления потока был применен метод wait(), то для «пробуждения» потока<br />
можно воспользоваться методом notify() или notifyAll(). Первый пробуждает один случайно выбранный спящий поток, а второй пытается<br />
пробудить их всех.<br />
<br />
Кроме рассмотренных выше методов, иногда бывает целесообразно использовать метод yield(), который приостанавливает работу текущего потока. Метод yield() не переводит поток в режим ожидания, как wait(), но предоставляет другим потокам возможность начать работать<br />
раньше, чем допустила бы Java-машина [фактически, поток, вызвавший yield() добровольно отдает свой квант процессорного времени, -<br />
прим. ред.]. Метод yield() статичный, так что прекратить с его помощью работу другого потока не получится.<br />
<br />
Методов остановки потоков тоже нет (ранее присутствовали методы stop(), resume(), suspend(), но сейчас они объявлены как «deprecated» -<br />
то есть нерекомендованными к использованию). На сегодня в Java принят уведомительный стиль остановки потока с помощью пары методов:<br />
уже известного нам interrupt(), применяемого снаружи, чтобы выставить флаг завершения и метода isInterrupted(), вызываемого изнутри<br />
потока, чтобы узнать состояние флага, свидетельствующего о том, что «пора закругляться».<br />
<br />
=== Мониторы и синхронизация ===<br />
Что такое «монитор», о котором говорилось выше? Нет, это не дисплей, это — объект, используемый как защелка, то есть в данный момент<br />
времени владеть монитором может только один поток. В случае, если поток завладел монитором, говорят, что он «вошел» в монитор, а все<br />
остальные потоки, пытающиеся это сделать, будут заморожены (часто говорят, что они «ждут» монитора) до тех пор, пока владелец монитора<br />
его не освободит, то есть не покинет.<br />
<br />
Если перейти к реалиям Java, то объектов типа монитор в явном виде просто нет! С каждым объектом связан неявный монитор, и чтобы<br />
завладеть им, необходимо вызвать метод или блок, помеченный ключевым словом synchronized. Как только поток входит в такой блок, он<br />
завладевает монитором объекта, переданного synchronized в качестве параметра. Так происходит и в классе ThirdThread, однако, в момент<br />
вызова метода wait(), монитор отпускается. Пример synchronized-метода представлен ниже:<br />
<source lang = "java"><br />
public synchronized boolean sameCheck() {<br />
if (a) {<br />
a = false; return true;<br />
} <br />
else {<br />
a = true; <br />
return false;<br />
}<br />
}<br />
</source><br />
В целом, синхронизация — это механизм, обеспечивающий монопольный доступ участка кода к некоторому объекту. Одним из первых<br />
способов, предложенных для синхронизации работы потоков, были семафоры, концепцию которых описал Дейкстра [Dijkstra] в 1965 году<br />
(часто говорят, что семафор — это классический синхронизированный примитив). Семафор используется для предоставления доступа к огра-<br />
ниченному количеству ресурсов. Как правило, у семафора есть две операции: P — занять ресурс и V — освободить ресурс. Он может быть<br />
реализован следующим образом:<br />
<source lang = "java"><br />
public class SimpleSemaphore {<br />
int counter;<br />
public SimpleSemaphore() {<br />
this.counter = 1;<br />
}<br />
<br />
public synchronized void p() throws InterruptedException {<br />
while (counter == 0) {<br />
wait();<br />
}<br />
counter = counter + 1;<br />
}<br />
<br />
public synchronized void v() throws InterruptedException {<br />
counter = counter - 1;<br />
notify();<br />
}<br />
}<br />
</source><br />
Можно, конечно, реализовывать семафоры самостоятельно, но проще воспользоваться специальной библиотекой java.util.concurrent.<br />
Кроме семафоров, она включает в себя еще много чего интересного.<br />
<br />
Отметим, что программа, в принципе, может не использовать ни один из методов синхронизации, обходясь методами wait() и notify().<br />
<br />
=== Взаимные блокировки ===<br />
На этом наш рассказ можно было бы и завершить, но чтобы у вас не сложилось впечатление, что в мире многопоточных приложений все<br />
так радужно, мы поговорим о неприятных последствиях синхронизации. Как только количество потоков начинает стремительно расти и<br />
возникает необходимость синхронизированного доступа к ограниченному кругу объектов в различной последовательности, будьте готовы к<br />
ошибкам типа deadlock — взаимным блокировкам.<br />
<br />
Взаимная блокировка — это ошибка, которая лучше всего описывается простой формулой: «Поток A держит монитор a и хочет захватить<br />
монитор b, а поток B держит монитор b и хочет захватить монитор a». В результате оба засыпают «мертвым сном».<br />
<br />
Ошибка очень противная и возникает обычно в нетривиальных алгоритмах. Лечится взаимная блокировка грамотным проектированием и профилактическими мерами, вроде следующей: всегда захватывайте мониторы в одном и том же порядке.<br />
<br />
Сегодня мы поговорили о двух способах создания потоков Java, разобрались с приоритетами, познакомились со средствами управления работой потоков и демонами, а также сделали небольшой обзор методов синхронизации. Для того, чтобы начать практическую работу с потоками, этого вполне достаточно. Желающим разобраться во всем этом глубже я рекомендую ознакомится с книгой «Concurrent Programming in Java: Design Principles and Patterns», автором которой является Дуг Ли [Doug Lea] — она считается одной из лучших по данной тематике.<br />
<br />
На этом мы заканчиваем обзор основ программирования на Java и в [[LXF89:Java_EE|следующий раз]] поговорим о серверных приложениях — приготовьтесь<br />
к Java Enterprise Edition!<br />
<br />
=== Литература ===<br />
* 1. П. Кью «Использование UNIX», ISBN 5-8275-0019-4<br />
* 2. В. Г. Олифер, Н. А. Олифер «Сетевые операционные системы», ISBN 5-272-00120-6<br />
* 3. Д. Бэкон, Т. Харрис «Операционные системы», ISBN 5-94723-969-8<br />
* 4. М. Фаулер «Архитектура корпоративных программных приложений», ISBN 5-8459-0579-6</div>Vanuan