LXF87-88:Java
(Начало, содержание) |
Vanuan (обсуждение | вклад) (→Потоки в Java) |
||
(не показаны 15 промежуточных версий 4 участников) | |||
Строка 1: | Строка 1: | ||
+ | {{Цикл/Java}} | ||
+ | |||
=== Потоки в Java === | === Потоки в Java === | ||
− | ''ЧАСТЬ 4: Завершая курс молодого Java-бойца, '''Антон Черноусов''' научит вас управлять | + | ''ЧАСТЬ 4: Завершая курс молодого Java-бойца, '''Антон Черноусов''' научит вас управлять потоками… Жаль, что не денежными.'' |
− | + | ||
− | C каждым днем появляются все более мощные процессоры, многоядерная архитектура которых стала основной темой ушедшего года, поэтому двухядерный процессор в ноутбуке уже никого не удивляет. С одной стороны | + | C каждым днем появляются все более мощные процессоры, многоядерная архитектура которых стала основной темой ушедшего года, поэтому двухядерный процессор в ноутбуке уже никого не удивляет. С одной стороны — это обстоятельство приближает возможности простого пользователя к возможностям «по-настоящему» больших систем. С другой (и рекламные буклеты об этом обычно молчат) — для того, чтобы использовать весь потенциал современных компьютеров, приложение должно «уметь» просчитать задачу фактически на двух или более процессорах. |
− | Создание эффективных алгоритмов для работы на многопроцессорных станциях | + | Создание эффективных алгоритмов для работы на многопроцессорных станциях — это большая и сложная работа. Несмотря на это, для любого программиста актуальна задача организации взаимодействия с медленными ресурсами (например, чтения, записи или копирования файлов, работы с принтером, сетью), так как немногие пользователи смирятся с тем, что их любимая программа «замирает» в момент выполнения какой-либо операции. |
− | Во избежание описанных проблем программа должна использовать потоки или процессы. Под процессом понимается заявка на | + | Во избежание описанных проблем программа должна использовать потоки или процессы. Под процессом понимается заявка на потребление всех видов ресурсов системы, кроме одного — процессорного времени, или иначе говоря, процесс — это запущенная на выполнение программа (такое определение дается в [[LXF87/88:Java#Литература|[1]]]). Поток рассматривается как самостоятельная активность внутри процесса, хотя существуют другие трактовки этого понятия, которые зависят от используемой операционной системы (см., например, [[LXF87/88:Java#Литература|[2]]]). Поток получил свое название по аналогии с потоком команд, поступающих в процессор; при выполнении потоки делят адресное пространство и выделенную память внутри одного процесса. Процессорное время распределяется между различными потоками операционной системой, точнее, одним из компонентов ее ядра — планировщиком. Более полно с понятием процессов и потоков и механизмов работы с ними с точки зрения операционной системы вы можете ознакомиться в книге [[LXF87/88:Java#Литература|[3]]]. |
− | + | ||
− | Давайте завершим наш экскурс в теорию и окунемся в реальность Java. Под процессом здесь принято понимать всеобъемлющий контекст выполнения, обеспечивающий высокий уровень изоляции охватываемых им данных от внешнего мира, а под потоком | + | Давайте завершим наш экскурс в теорию и окунемся в реальность Java. Под процессом здесь принято понимать всеобъемлющий контекст выполнения, обеспечивающий высокий уровень изоляции охватываемых им данных от внешнего мира, а под потоком — более «легковесный» активный агент; в контексте одного процесса может функционировать целое множество потоков [[LXF87/88:Java#Литература|[4]]]. Планирование потоков в Java обеспечивается внутренними механизмами JVM. |
=== Поток, он же thread === | === Поток, он же thread === | ||
+ | В Java существует два способа работы с потоками: первый заключается в реализации интерфейса Runnable, второй связан с наследованием | ||
+ | класса Thread, который уже реализует данный интерфейс. В обоих случаях класс должен предоставлять реализацию метода run(). Ниже | ||
+ | приведен пример класса, реализующего поток через наследование класса Thread: | ||
+ | <source lang = "java"> | ||
+ | public class FirstThread extends Thread { | ||
+ | public void run(){ | ||
+ | for (int i = 1 ; i < 30; i++) | ||
+ | System.out.println("It is in thread "+ i); | ||
+ | } | ||
+ | } | ||
+ | </source> | ||
+ | Собственно, метод run() и должен содержать некоторый набор инструкций (разумеется, на языке Java), которые вы хотите выполнить | ||
+ | в отдельном потоке. Например, если вы реализуете функцию копирования файла (а пользователь, скажем, копирует ISO-образ объемом | ||
+ | 600 Мб), желательно, чтобы эта операция выполнялась в отдельном потоке, запуск которого можно производить следующим образом: | ||
+ | <source lang = "java"> | ||
+ | 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"); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </source> | ||
+ | При выполнении метода main() класса ConsoleToThread создается объект-поток FirstThread, который запускается на выполнение методом start() [заметьте — метод run() никогда не вызывается явно, — прим.ред.]. Метод join() используется в случае, когда необходимо «дождаться» завершения потока. Завершение работы потока происходит при выходе из метода run(), как явном (например, посредством return), так и неявном (если внутри метода возникло и не было обработано какое-то исключение). | ||
+ | |||
+ | Имейте в виду (это важно!): повторный запуск уже отработавшего потока приведет к исключению 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 | ||
+ | * 2. В. Г. Олифер, Н. А. Олифер «Сетевые операционные системы», ISBN 5-272-00120-6 | ||
+ | * 3. Д. Бэкон, Т. Харрис «Операционные системы», ISBN 5-94723-969-8 | ||
+ | * 4. М. Фаулер «Архитектура корпоративных программных приложений», ISBN 5-8459-0579-6 |
Текущая версия на 18:39, 13 мая 2009
|
|
|
- Метамодернизм в позднем творчестве В.Г. Сорокина
- ЛитРПГ - последняя отрыжка постмодерна
- "Ричард III и семиотика"
- 3D-визуализация обложки Ridero создаем обложку книги при работе над самиздатом.
- Архитектура метамодерна - говоря о современном искусстве, невозможно не поговорить об архитектуре. В данной статье будет отмечено несколько интересных принципов, характерных для построек "новой волны", столь притягательных и скандальных.
- Литература
- Метамодерн
- Рокер-Прометей против изначального зла в «Песне про советскую милицию» Вени Дркина, Автор: Нина Ищенко, к.ф.н, член Союза Писателей ЛНР - перепубликация из журнала "Топос".
- Как избавиться от комаров? Лучшие типы ловушек.
- Что делать если роблокс вылетает на windows
- Что делать, если ребенок смотрит порно?
- Почему собака прыгает на людей при встрече?
- Какое масло лить в Задний дифференциал (мост) Visco diff 38434AA050
- О чем может рассказать хвост вашей кошки?
- Верветки
- Отчетность бюджетных учреждений при закупках по Закону № 223-ФЗ
- Срок исковой давности как правильно рассчитать
- Дмитрий Патрушев минсельхоз будет ли преемником Путина
- Кто такой Владислав Поздняков? Что такое "Мужское Государство" и почему его признали экстремистским в России?
- Как правильно выбрать машинное масло в Димитровграде?
- Как стать богатым и знаменитым в России?
- Почему фильм "Пипец" (Kick-Ass) стал популярен по всему миру?
- Как стать мудрецом?
- Как правильно установить FreeBSD
- Как стать таким как Путин?
- Где лучше жить - в Димитровграде или в Ульяновске?
- Почему город Димитровград так называется?
- Что такое метамодерн?
- ВАЖНО! Временное ограничение движения автотранспортных средств в Димитровграде
- Тарифы на электроэнергию для майнеров предложено повысить
Содержание |
[править] Потоки в 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