Новый поток java. Многопоточное программирование

В этой статье рассказывается, как реализовать потоки, используя язык программирования Java. Перед тем как рассмотреть детали программирования на Java, давайте приступим к обзору о потоках в общем.

Если сказать просто, то поток(thread) – это путь программного выполнения. Большинство программ, написанных сегодня, запускаются одним потоком, проблемы начинают возникать, когда несколько событий или действий должны произойти в одно время. Допустим, например, программа не способна рисовать картинку пока выполняет чтение нажатия клавиш. Программа должна уделять всё своё внимание клавиатуре, вследствие чего отсутствует возможность обрабатывать более одного события одновременно. Идеальным решением для этой проблемы может служить возможность выполнения двух или более разделов программы в одно время. Потоки позволяют нам это сделать.

Многопоточные приложения предоставляют мощь при запуске многих потоков в рамках одной программы. С логической точки зрения, многопоточность означает, что несколько строк из одной и той же программы могут быть выполнены в одно и то же время, однако, это не то же самое, что запустить программу дважды и сказать, что несколько строк кода выполняются в одно время. В этом случае, операционная система обрабатывает две программы раздельно и как отдельные процессы. В Unix, разветвляющий(forking) процесс создаёт дочерний процесс с разным адресным пространством для кода и данных. Вместе с тем, fork() создаёт много накладок для операционной системы, это влечёт за собой интенсивную нагрузку на процессор. При запуске потока, эффективный путь выполнения создаётся за счёт распределения исходного пространство данных родителя. Идея совместного использования данных очень выгодна, но вызывает некоторые вопросы, которые мы обсудим позже.

Создание потоков

Создатели Java милостиво предоставили две возможности создания потоков: реализовать(implementing) интерфейс и расширить(extending) класс. Расширение класса это путь наследования методов и переменных класса родителя. В этом случае можно наследоваться только от одного родительского класса. Это ограничение внутри Java можно побороть реализацией интерфейса, который является наиболее распространённым способом создания потоков. (Заметим, что способ наследования позволяет только запустить класс как поток. Это позволяет классу только выполнить start() и т.п.).

Интерфейсы дают возможность программистам заложить фундамент одного класса. Они используются для разработки требований к набору классов для реализации. Интерфейс устанавливает правила. Разные наборы классов, которые реализуют интерфейс, должны следовать этим правилам.

Есть некоторые отличия между классом и интерфейсом. Во-первых, интерфейс может только содержать абстрактные методы и/или static final переменные (константы). Классы, с другой стороны, могут реализовывать методы и содержать переменные, которые не выступают в качестве констант. Во-вторых, интерфейс не может реализовывать никаких методов. Класс, который реализовывает интерфейс, должен реализовать все методы, которые описаны в интерфейсе. У интерфейса есть возможность расширяться за счёт других интерфейсов, и (в отличие от классов) могут расширяться от нескольких интерфейсов. К тому же, экземпляр интерфейса не может быть создан, используя, оператор new; например, Runnable a = new Runnable(); не разрешается .

Для первого способа создания потока необходимо просто наследоваться от класса Thread. Делайте так, только если классу нужно только выполниться как отдельному потоку и никогда не понадобиться наследоваться от другого класса. Класс Thread определён в пакете java.lang, который необходимо импортировать, что бы наши классы знали о его описании:


import java.lang.*;
public class Counter extends Thread {
public void run() {
....
}
}

Пример выше создаёт новый класс Counter, который расширяет класс Thread и подменяет метод Thread.run() для своей реализации. В методе run() происходит вся работа класса Counter как потока. Такой же класс можно создать, реализуя интерфейс Runnable:


import java.lang.*;
public class Counter implements Runnable {
Thread T;
public void run() {
....
}
}

Здесь осуществляется абстрактный метод run(), который описан в интерфейсе Runnable. Отметим, что у нас есть экземпляр класса Thread, переменная класса Counter. Единственное отличие этих двух методов заключается в том, что реализация Runnable является более гибкой для создания класса Counter. В примере, который описан выше, есть возможность расширения класса Counter, если в этом есть такая необходимость. Большинство классов, которые должны выполняться как потоки, реализуют Runnable, поскольку они, вероятно, могут расширить свою функциональность за счёт другого класса.

Не подумайте, что интерфейс Runnable выполняет какую-то реальную работу, когда поток запускается. Это всего лишь класс, созданный для того, что бы дать представление о классе Thread. На самом деле, это очень небольшой, содержащий только один абстрактный метод интерфейс. Это описание интерфейса Runnable, взятое из исходных кодов Java:


package java.lang;
public interface Runnable {
public abstract void run();
}

Это и всё, что есть в интерфейсе Runnable. Интерфейс – это всего лишь описание, которое классы должны реализовать. Итак, Runnable, заставляет только запустить метод run(). Вследствие этого, большая часть работы полагается на класс Thread. Более пристальный взгляд на класс Thread даст представление о том, что на самом деле происходит:


public class Thread implements Runnable {
...
public void run() {
if (target != null) {
target.run();
}
}
...
}

Во фрагменте кода, который представлен выше видно, что класс Thread, так же реализует интерфейс Runnable. Thread.run() выполняет проверку, что бы удостовериться в том, что этот класс (класс, который выполняется как поток) не равен null, и только потом выполняет метод run(). Когда это произойдёт, то метод run() запустит поток.

Запуск и остановка

Различные способы создания объектов потока сейчас очевидны, мы продолжим дискуссию о реализации потоков, начиная с путей запуска и остановки их, используя, маленький applet, который содержит поток для иллюстрации этих механизмов:

Апплет выше, начнёт отсчёт с нуля и будет отображать это и на экране, и в консоль. На первый взгляд может сложиться впечатление, что программа начинает отсчёт и показывает каждую цифру, но это не так. При более внимательном рассмотрении этого апплета будет выявлено, как на самом деле работает этот апплет.

В этом случае, класс CounterThread был вынужден реализовать интерфейс Runnnable, что бы дальше была возможность расширить класс Applet. Все апплеты начинают свою работу с метода init(), переменная Cout инициализируется нулём и создаётся новый объект класса Thread. Передавая this в конструктор класса Thread, таким образом, новый поток будет знать какой объект запускается. В этом случает this это ссылка на CounterThread. После того как поток создан его нужно запустить. Вызываем метод start(), который в свою очередь вызывает метод run() объекта CounterThread, то есть CounterThread.run(). Сразу выполниться метод start() и в это же время начнёт свою работу поток. Заметим, что в методе run() бесконечный цикл. Он бесконечен, потому что, как только выполниться метод run(), то поток закончит работу. Метод run() будет инкрементировать переменную Count, ожидать(sleep) 10 секунд и посылать запрос на обновление экрана апплета.

Заметим, что вызова метода sleep именно в потоке является очень важным. Если это не так, то программа займёт всё процессорное время для своего процесса и не даст возможности любым другим методам, например методам, выполниться. Другой способ остановить выполнение потока это вызвать метод stop(). В данном примере, поток останавливается, когда происходит нажатие мыши в пределах апплета. В зависимости от скорости компьютера, на котором запущен апплет, не все числа будут отображены, потому что инкрементирование происходит независимо от прорисовки апплета. Апплет может не обновляться после каждого запроса на прорисовку, так как ОС может поставить запрос в очередь запросов и последующие запросы на обновление будут удовлетворены с одним запросом. Пока запросы на перерисовку собираются в очередь, переменная Count продолжает увеличиваться, но не отображается.

Приостановка и возобновление

Когда поток остановлен с использованием метода stop() он уже не может быть возобновлён с использованием метода start(), сразу после вызова метода stop() происходит уничтожение выполняющегося потока. Вместо этого вы можете приостановить выполнение потока, используя метод sleep() на определённый отрезок времени и потом выполнение потока продолжится, когда выйдет время. Но это не самое лучшее решение, если поток необходимо запустить, когда произойдёт определённое условие. Для этого, используется метод suspend(), который даёт возможность временно прекратить выполнение потока и метод resume(), который позволяет продолжить выполнение потока. Следующий апплет является изменением апплета, который был дан выше, но с использованием методов suspend() и resume():


public class CounterThread2 extends Applet implements Runnable
{
Thread t;
int Count;
boolean suspended;
public boolean mouseDown(Event e,int x, int y)
{
if(suspended)
t.resume();
else
t.suspend();
suspended = !suspended;
return true;
}
...
}

Для того чтобы сохранить текущее состояние апплета используется логическая(boolean) переменная suspended. Характеристики разных состояний апплета является важной частью, потому что некоторые методы могут выкидывать исключения, если они вызываются не из того состояния. Например, если поток запущен и остановлен, вызов метода start() приведёт к исключению IllegalThreadStateException .

Планирование

В Java есть Планировщик Потоков(Thread Scheduler), который контролирует все запущенные потоки во всех программах и решает, какие потоки должны быть запущены, и какая строка кода выполняться. Существует две характеристики потока, по которым планировщик идентифицирует процесс. Первая, более важная, это приоритет потока, другая, является-ли поток демоном(daemon flag). Простейшее правило планировщика, это если запущены только daemon потоки, то Java Virtual Machine (JVM) вызгрузиться. Новые потоки наследуют приоритет и daemon flag от потока, который его создал. Планировщик определяет какой поток должен быть запущен, анализируя приоритет всех потоков. Потоку с наивысшим приоритетом позволяется выполниться раньше, нежели потокам с более низкими приоритетами.

Планировщик может быть двух видов: с преимуществом и без. Планировщик с преимуществом предоставляет определённый отрезок времени для всех потоков, которые запущены в системе. Планировщик решает, какой поток следующий запуститься или возобновить работу через некоторый постоянный период времени. Когда поток запуститься, через этот определённый промежуток времени, то выполняющийся поток будет приостановлен и следующий поток возобновит свою работу. Планировщик без приоритета решает, какой поток должен запуститься и выполняться до того, пока не закончит свою работу. Поток имеет полный контроль над системой настолько долго, сколько ему захочется. Метод yield() можно использовать для того чтобы принудить планировщик выполнить другой поток, который ожидает своей очереди. В зависимости от системы, на которой запущена Java, планировщик может быть либо с преимуществом, либо без него.

Приоритеты

Планировщик определяет, какой поток должен запуститься, основываясь на номер приоритета, назначенный каждому потоку. Приоритет потока может принимать значения от 1 до 10. По умолчанию, значение приоритета для потока является Thread.NORM_PRIORITY, которому соответствует значение 5. Так же доступны две других static переменных: Thread.MIN_PRIORITY, значение 1, и Thread.MAX_PRIORITY – 10. Метод getPriority() может использоваться для получения текущего значения приоритета соответствующего потока.

Daemon потоки

Такие потоки иногда ещё называются “службами”, которые обычно запускаются с наименьшим приоритетом и обеспечивают основные услуги для программы или программ, когда деятельность компьютера понижается. Примером такого потока может служить сборщик мусора. Этот поток, предусмотрен JVM, сканирует программы на наличие переменных, к которым больше никогда не придется обращаться, и освобождает их ресурсы, возвращая их системе. Поток может стать daemon потоком, передав булево значение true в метод setDaemon(). Если принято значение false, то поток становится обычным пользовательским потоком. Тем не менее, это необходимо сделать до того как поток запустится.

Пример планирования

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

Заключение

Использование потоков в Java позволит программистам более гибко использовать преимущества Java в их программах. Простота создания, настраивания и запуска потоков даст Java программистам возможность разрабатывать переносимые и мощные апплеты/приложения, которые невозможно выполнить в других языках третьего поколения. Потоки позволяют любым программам выполнять несколько задач как одну. В интернет ориентированных(Internet-aware) языках, таких как Java, это очень важный инструмент.

Об авторе

Donald G. Drake программирует на Java начиная с альфа релиза(alpha release). Drake написал апплеты для специальных веб-сайтов. Наиболее популярна его версия TickerTape (http://www.netobjective.com/java/TickerTapeInfo.html), которая может быть широко сконфигурирована для внедрения в любые веб-страницы. Drake имеет степень бакалавра наук в области компьютерных наук в John Carroll University. В настоящее время он получает степень магистра наук в DePaul University.

В теоретических трудах по многопоточности вы можете встретить описание трех задач, которые по словам авторов, покрывают все возможные задачи многопоточности — задача производитель-потребитель, задача читатели-писатели, задача обедающие философы. Аллегория красивая и по своему достаточно неплохая, но на мой взгляд, для неокрепшего молодого программиста, совершенно ничего не говорящая. Посему опишу проблемы со своей колокольни. Проблем всего две.

Проблема первая — доступ к одному ресурсу из нескольких потоков. Мы уже описывали проблему с одной лопатой. Можете расширить вариант — есть один бак с водой (с одним краником), 25 жаждущих пить рудокопов и 5 кружек на всех. Придется договариваться, иначе смертоубийство может начаться. Причем надо не только сохранить кружки в целостности — надо еще организовать все так, чтобы всем удалось попить. Это частично переходит на проблему номер два.
Проблема вторая — синхронизация взаимодействия. Как-то мне предложили задачу — написать простую программу, чтобы два потока играли в пинг-понг. Один пишет «Пинг», а второй — «Понг». Но они это должны делать по очереди. А теперь представим, что надо сделать такую же задачу, но на 4 потока — играем пара на пару.

Т.е. постановка проблем весьма несложная. Раз — надо организовать упорядоченный и безопасный доступ к разделяемому ресурсу. Два — надо выполнять потоки в какой-то очередности.
Дело за реализацией. И вот тут нас подстерегает много сложностей, про которые с предыханием и говорят (и может не зря). Начнем с разделяемого ресурса.

Совместный ресурс для нескольких потоков

Предлагаю сразу продемонстрировать проблему на несложном примере. Его задача — запустить 200 потоков класса CounterThread . Каждый поток получает ссылку на один единственный объект Counter . В процессе выполнения поток вызывает у этого объекта метод increaseCounter одну тысячу раз. Метод увеличивает переменную counter на 1. Запустив 200 потоков мы ожидаем их окончания (просто засыпаем на 1 секунду — этого вполне достаточно). И в конце печатаем результат. Посмотрите код — по-моему, там все достаточно прозрачно:

<200; i++) { CounterThread ct = new CounterThread(counter); ct.start(); } Thread.sleep(1000); System.out.println("Counter:" + counter.getCounter()); } } class Counter { private long counter = 0L; public void increaseCounter() { counter++; } public long getCounter() { return counter; } } class CounterThread extends Thread { private Counter counter; public CounterThread(Counter counter) { this.counter = counter; } @Override public void run() { for(int i=0; i<1000; i++) { counter.increaseCounter(); } } }

public class CounterTester

for (int i = 0 ; i < 200 ; i ++ ) {

ct . start () ;

Thread . sleep (1000 ) ;

class Counter

private long counter = 0L ;

public void increaseCounter () {

counter ++ ;

public long getCounter () {

return counter ;

private Counter counter ;

this . counter = counter ;

@ Override

public void run () {

for (int i = 0 ; i < 1000 ; i ++ ) {

По логике мы должны получить следующий результат — 200 потоков по 1000 прибавлений = 200000. Но, о ужас, это совсем не так. У меня результаты бывают разные, но явно не 200000. В чем же проблема? Проблема в том, что мы из 200 потоков одновременно пытаемся вызвать метод increaseCounter . На первый взгляд в нем ничего страшного не происходит — мы просто прибавляем к переменной counter единицу. Что же тут такого ужасного?
Ужасно то, что безобидный на первый взгляд код прибавления единицы, на самом деле выполняется не за один шаг. Сначала мы считываем значение переменной в регистр, потом прибавляем к нему единицу, потом записываем результат обратно в переменную. Как видите, шагов больше, чем один (по секрету — их даже больше чем три, которые я описал). И вот теперь представим, что два потока (или даже больше) одновременно считали значение переменной — например там было значение 99. Теперь оба потока прибавляют к 99 по единице, получают оба 100 и оба записывают это значение в переменную. Что там получается? Нетрудно видеть, что будет 100. А должно быть 101. Может быть даже хуже, если какой-то поток «умудрился» считать 98 и «застрял» в очереди потоков на исполнение. Мы тогда даже 100 не получим. Неувязочка 🙂

Доступ к разделяемому ресурсу — это одна из самых больших проблем многопоточности. Потому что она весьма коварна. Можно сделать все очень надежно, но тогда производительность упадет. А как только даешь «слабину» (сознательно, для производительности), обязательно возникнет ситуация, что «слабина» вылезет во всей своей красе.

Волшебное слово — synchronized

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

package edu.javacourse.counter; public class CounterTester { public static void main(String args) throws InterruptedException { Counter counter = new Counter(); for(int i=0; i<200; i++) { CounterThread ct = new CounterThread(counter); ct.start(); } Thread.sleep(1000); System.out.println("Counter:" + counter.getCounter()); } } class Counter { private long counter = 0L; public synchronized void increaseCounter() { counter++; } public long getCounter() { return counter; } } class CounterThread extends Thread { private Counter counter; public CounterThread(Counter counter) { this.counter = counter; } @Override public void run() { for(int i=0; i<1000; i++) { counter.increaseCounter(); } } }

package edu . javacourse . counter ;

public class CounterTester

public static void main (String args ) throws InterruptedException {

Counter counter = new Counter () ;

for (int i = 0 ; i < 200 ; i ++ ) {

CounterThread ct = new CounterThread (counter ) ;

ct . start () ;

Thread . sleep (1000 ) ;

System . out . println ("Counter:" + counter . getCounter () ) ;

class Counter

private long counter = 0L ;

public synchronized void increaseCounter () {

counter ++ ;

public long getCounter () {

return counter ;

class CounterThread extends Thread

private Counter counter ;

public CounterThread (Counter counter ) {

this . counter = counter ;

@ Override

public void run () {

for (int i = 0 ; i < 1000 ; i ++ ) {

counter . increaseCounter () ;

И … о чудо. Все заработало. Мы получаем ожидаемый результат — 200000. Что же делает это волшебное слово — synchronized ?
Слово synchronized говорит о том, что прежде чем поток сможет вызвать этот метод у нашего объекта, он должен «захватить» наш объект и потом выполнить нужный метод. Еще раз и внимательно (иногда предлагается несколько иной подход, который на мой взгляд, крайне опасен и ошибочен — чуть позже опишу) — сначала поток «захватывает» (лочит — от слова lock — замок, блокировать) объект-монитор (в нашем случае это объект класса Counter ) и только после этого поток сможет выполнить метод increaseCounter . Эксклюзивно, в полном одиночестве без конкурентов.
Существует иная трактовка synchronized , которая может ввести в заблуждение — она звучит как-то так: в synchronized метод не может зайти несколько потоков одновременно. Это НЕВЕРНО. Потому как тогда получается, что если у класса несколько методов synchronized , то одновременно можно выполнять два разных метода одного объекта, помеченные как synchronized . Это НЕВЕРНО. Если у класса 2, 3 и более методов synchronized , то при выполнении хотя бы одного, блокируется весь объект. Это значит, что все методы, обозначенные как synchronized недоступны для других потоков. Если метод не обзозначен так. то не проблема — выполняйте на здоровье.
И еще раз — сначала «захватили», потом выполнили метод, потом «отпустили». Теперь объект свободен и кто первый успел из потоков его захватить — тот и прав.
В случае если метод объявлен как static , то объектом-монитором становится класс целиком и доступ к нему блокируется на уровне всех объектов этого класса.

При обсуждении статьи мне указали на некорректность, которую я сознательно допустил (для простоты), но наверно есть смысл о ней упомянуть. Речь идет о методе getCounter . Строго говоря, он тоже должен быть обозначен как synchronized , потому что в момент изменения нашей переменной какой-то другой поток захочет ее прочитать. И чтобы не было проблем, доступ к этой переменной надо делать синхронизированным во всех метода.
Хотя что касается getCounter , то здесь можно использовать еще более интересную особенность — атомарность операций. О ней можно прочитать в статье Atomic access . Основная мысль — чтение и запись некоторых элементарных типов и ссылок производится за один шаг и в принципе безопасна. Если бы поле counter было например int , то читать можно было бы и не в синхронном методе. Для типа long и double мы должны объявить переменную counter как volatile . Почему это может быть любопытно — надо учесть, что int состоит из 4 байт и можно представить ситуацию, что число будет записано не за один шаг. Но это исключительно теоретически — JVM нам гарантирует, что чтение и запись элементарного типа int делает за один шаг и ни один поток не сможет вклиниться в эту операцию и что-то испортить.

Есть и другой способ использования слова synchronized — не в описании метода, а внутри кода. Давайте еще раз изменим наш пример в части метода increaseCounter .

package edu.javacourse.counter; public class CounterTester { public static void main(String args) throws InterruptedException { Counter counter = new Counter(); for(int i=0; i<200; i++) { CounterThread ct = new CounterThread(counter); ct.start(); } Thread.sleep(1000); System.out.println("Counter:" + counter.getCounter()); } } class Counter { private long counter = 0L; public void increaseCounter() { synchronized(this) { counter++; } } public long getCounter() { return counter; } } class CounterThread extends Thread { private Counter counter; public CounterThread(Counter counter) { this.counter = counter; } @Override public void run() { for(int i=0; i<1000; i++) { counter.increaseCounter(); } } }

package edu . javacourse . counter ;

public class CounterTester

public static void main (String args ) throws InterruptedException {

Counter counter = new Counter () ;

for (int i = 0 ; i < 200 ; i ++ ) {

Здравствуйте! В этой статье я вкратце расскажу вам о процессах, потоках, и об основах многопоточного программирования на языке Java.
Наиболее очевидная область применения многопоточности – это программирование интерфейсов. Многопоточность незаменима тогда, когда необходимо, чтобы графический интерфейс продолжал отзываться на действия пользователя во время выполнения некоторой обработки информации. Например, поток, отвечающий за интерфейс, может ждать завершения другого потока, загружающего файл из интернета, и в это время выводить некоторую анимацию или обновлять прогресс-бар. Кроме того он может остановить поток загружающий файл, если была нажата кнопка «отмена».

Еще одна популярная и, пожалуй, одна из самых хардкорных областей применения многопоточности – игры. В играх различные потоки могут отвечать за работу с сетью, анимацию, расчет физики и т.п.

Давайте начнем. Сначала о процессах.

Процессы

Процесс - это совокупность кода и данных, разделяющих общее виртуальное адресное пространство. Чаще всего одна программа состоит из одного процесса, но бывают и исключения (например, браузер Chrome создает отдельный процесс для каждой вкладки, что дает ему некоторые преимущества, вроде независимости вкладок друг от друга). Процессы изолированы друг от друга, поэтому прямой доступ к памяти чужого процесса невозможен (взаимодействие между процессами осуществляется с помощью специальных средств).

Для каждого процесса ОС создает так называемое «виртуальное адресное пространство», к которому процесс имеет прямой доступ. Это пространство принадлежит процессу, содержит только его данные и находится в полном его распоряжении. Операционная система же отвечает за то, как виртуальное пространство процесса проецируется на физическую память.

Схема этого взаимодействия представлена на картинке. Операционная система оперирует так называемыми страницами памяти, которые представляют собой просто область определенного фиксированного размера. Если процессу становится недостаточно памяти, система выделяет ему дополнительные страницы из физической памяти. Страницы виртуальной памяти могут проецироваться на физическую память в произвольном порядке.

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

Потоки

Один поток – это одна единица исполнения кода. Каждый поток последовательно выполняет инструкции процесса, которому он принадлежит, параллельно с другими потоками этого процесса.

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

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

Вот как это выглядит:

Цветные квадраты на рисунке – это инструкции процессора (зеленые – инструкции главного потока, синие – побочного). Выполнение идет слева направо. После запуска побочного потока его инструкции начинают выполняться вперемешку с инструкциями главного потока. Кол-во выполняемых инструкций за каждый подход не определено.

То, что инструкции параллельных потоков выполняются вперемешку, в некоторых случаях может привести к конфликтам доступа к данным. Проблемам взаимодействия потоков будет посвящена следующая статья, а пока о том, как запускаются потоки в Java…

Запуск потоков

Каждый процесс имеет хотя бы один выполняющийся поток. Тот поток, с которого начинается выполнение программы, называется главным. В языке Java, после создания процесса, выполнение главного потока начинается с метода main(). Затем, по мере необходимости, в заданных программистом местах, и при выполнении заданных им же условий, запускаются другие, побочные потоки.

В языке Java поток представляется в виде объекта-потомка класса Thread. Этот класс инкапсулирует стандартные механизмы работы с потоком.

Запустить новый поток можно двумя способами:

Способ 1

Создать объект класса Thread, передав ему в конструкторе нечто, реализующее интерфейс Runnable. Этот интерфейс содержит метод run(), который будет выполняться в новом потоке. Поток закончит выполнение, когда завершится его метод run().

Выглядит это так:

class SomeThing //Нечто, реализующее интерфейс Runnable implements Runnable //(содержащее метод run ()) { public void run () { System.out.println(); } } public class Program //Класс с методом main () { static SomeThing mThing; //mThing - объект класса, реализующего интерфейс Runnable { mThing = new SomeThing(); Thread myThready = new Thread(mThing); //Создание потока "myThready" myThready.start(); //Запуск потока System.out.println("Главный поток завершён..." ); } }

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

public class Program //Класс с методом main (). { public static void main (String args) { //Создание потока Thread myThready = new Thread(new Runnable() { public void run () //Этот метод будет выполняться в побочном потоке { System.out.println("Привет из побочного потока!" ); } }); myThready.start(); //Запуск потока System.out.println("Главный поток завершён..." ); } }
Способ 2

Создать потомка класса Thread и переопределить его метод run():

class AffableThread extends Thread { @Override public void run () //Этот метод будет выполнен в побочном потоке { System.out.println("Привет из побочного потока!" ); } } public class Program { static AffableThread mSecondThread; public static void main (String args) { mSecondThread = new AffableThread(); //Создание потока mSecondThread.start(); //Запуск потока System.out.println("Главный поток завершён..." ); } }

В приведённом выше примере в методе main() создается и запускается еще один поток. Важно отметить, что после вызова метода mSecondThread.start() главный поток продолжает своё выполнение, не дожидаясь пока порожденный им поток завершится. И те инструкции, которые идут после вызова метода start(), будут выполнены параллельно с инструкциями потока mSecondThread.

Для демонстрации параллельной работы потоков давайте рассмотрим программу, в которой два потока спорят на предмет философского вопроса «что было раньше, яйцо или курица?». Главный поток уверен, что первой была курица, о чем он и будет сообщать каждую секунду. Второй же поток раз в секунду будет опровергать своего оппонента. Всего спор продлится 5 секунд. Победит тот поток, который последним изречет свой ответ на этот, без сомнения, животрепещущий философский вопрос. В примере используются средства, о которых пока не было сказано (isAlive() sleep() и join()). К ним даны комментарии, а более подробно они будут разобраны дальше.

class EggVoice extends Thread { @Override public void run () { for (int i = 0 ; i < 5 ; i++) { try { sleep(1000 ); }catch (InterruptedException e){} System.out.println("яйцо!" ); } //Слово «яйцо» сказано 5 раз } } public class ChickenVoice //Класс с методом main () { static EggVoice mAnotherOpinion; //Побочный поток public static void main (String args) { mAnotherOpinion = new EggVoice(); //Создание потока System.out.println("Спор начат..." ); mAnotherOpinion.start(); //Запуск потока for (int i = 0 ; i < 5 ; i++) { //Приостанавливает поток на 1 секунду }catch (InterruptedException e){} System.out.println("курица!" ); } //Слово «курица» сказано 5 раз if (mAnotherOpinion.isAlive()) //Если оппонент еще не сказал последнее слово { try { mAnotherOpinion.join(); //Подождать пока оппонент закончит высказываться. }catch (InterruptedException e){} System.out.println("Первым появилось яйцо!" ); } else //если оппонент уже закончил высказываться { System.out.println("Первой появилась курица!" ); } System.out.println("Спор закончен!" ); } } Консоль: Спор начат... курица! яйцо! яйцо! курица! яйцо! курица! яйцо! курица! яйцо! курица! Первой появилась курица! Спор закончен!

В приведенном примере два потока параллельно в течении 5 секунд выводят информацию на консоль. Точно предсказать, какой поток закончит высказываться последним, невозможно. Можно попытаться, и можно даже угадать, но есть большая вероятность того, что та же программа при следующем запуске будет иметь другого «победителя». Это происходит из-за так называемого «асинхронного выполнения кода». Асинхронность означает то, что нельзя утверждать, что какая-либо инструкция одного потока, выполнится раньше или позже инструкции другого. Или, другими словами, параллельные потоки независимы друг от друга, за исключением тех случаев, когда программист сам описывает зависимости между потоками с помощью предусмотренных для этого средств языка.

Теперь немного о завершении процессов…

Завершение процесса и демоны

В Java процесс завершается тогда, когда завершается последний его поток. Даже если метод main() уже завершился, но еще выполняются порожденные им потоки, система будет ждать их завершения.

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

Объявить поток демоном достаточно просто - нужно перед запуском потока вызвать его метод setDaemon(true) ;
Проверить, является ли поток демоном, можно вызвав его метод boolean isDaemon() ;

Завершение потоков

В Java существуют (существовали) средства для принудительного завершения потока. В частности метод Thread.stop() завершает поток незамедлительно после своего выполнения. Однако этот метод, а также Thread.suspend(), приостанавливающий поток, и Thread.resume(), продолжающий выполнение потока, были объявлены устаревшими и их использование отныне крайне нежелательно. Дело в том что поток может быть «убит» во время выполнения операции, обрыв которой на полуслове оставит некоторый объект в неправильном состоянии, что приведет к появлению трудноотлавливаемой и случайным образом возникающей ошибке.

Вместо принудительного завершения потока применяется схема, в которой каждый поток сам ответственен за своё завершение. Поток может остановиться либо тогда, когда он закончит выполнение метода run(), (main() - для главного потока) либо по сигналу из другого потока. Причем как реагировать на такой сигнал - дело, опять же, самого потока. Получив его, поток может выполнить некоторые операции и завершить выполнение, а может и вовсе его проигнорировать и продолжить выполняться. Описание реакции на сигнал завершения потока лежит на плечах программиста.

Java имеет встроенный механизм оповещения потока, который называется Interruption (прерывание, вмешательство), и скоро мы его рассмотрим, но сначала посмотрите на следующую программку:

Incremenator - поток, который каждую секунду прибавляет или вычитает единицу из значения статической переменной Program.mValue. Incremenator содержит два закрытых поля – mIsIncrement и mFinish. То, какое действие выполняется, определяется булевой переменной mIsIncrement - если оно равно true, то выполняется прибавление единицы, иначе - вычитание. А завершение потока происходит, когда значение mFinish становится равно true.

{ //О ключевом слове volatile - чуть ниже private volatile boolean mIsIncrement = true ; private volatile boolean mFinish = false ; { mIsIncrement = !mIsIncrement; } public void finish () //Инициирует завершение потока { mFinish = true ; } @Override public void run () { do { if (!mFinish) //Проверка на необходимость завершения { if (mIsIncrement) Program.mValue++; //Инкремент else Program.mValue--; //Декремент //Завершение потока try { Thread.sleep(1000 ); }catch (InterruptedException e){} } while (true ); } } public class Program { //Объект побочного потока public static void main (String args) //Создание потока //с интервалом в i*2 секунд for (int i = 1 ; i <= 3 ; i++) { try { Thread.sleep(i*2 *1000 ); //Переключение действия } mInc.finish(); //Инициация завершения побочного потока

Взаимодействовать с потоком можно с помощью метода changeAction() (для смены вычитания на сложение и наоборот) и метода finish() (для завершения потока).

В объявлении переменных mIsIncrement и mFinish было использовано ключевое слово volatile (изменчивый, не постоянный). Его необходимо использовать для переменных, которые используются разными потоками. Это связано с тем, что значение переменной, объявленной без volatile, может кэшироваться отдельно для каждого потока, и значение из этого кэша может различаться для каждого из них. Объявление переменной с ключевым словом volatile отключает для неё такое кэширование и все запросы к переменной будут направляться непосредственно в память.

В этом примере показано, каким образом можно организовать взаимодействие между потоками. Однако есть одна проблема при таком подходе к завершению потока - Incremenator проверяет значение поля mFinish раз в секунду, поэтому может пройти до секунды времени между тем, когда будет выполнен метод finish(), и фактическим завершения потока. Было бы замечательно, если бы при получении сигнала извне, метод sleep() возвращал выполнение и поток незамедлительно начинал своё завершение. Для выполнения такого сценария существует встроенное средство оповещения потока, которое называется Interruption (прерывание, вмешательство).

Interruption

Класс Thread содержит в себе скрытое булево поле, подобное полю mFinish в программе Incremenator, которое называется флагом прерывания. Установить этот флаг можно вызвав метод interrupt() потока. Проверить же, установлен ли этот флаг, можно двумя способами. Первый способ - вызвать метод bool isInterrupted() объекта потока, второй - вызвать статический метод bool Thread.interrupted(). Первый метод возвращает состояние флага прерывания и оставляет этот флаг нетронутым. Второй метод возвращает состояние флага и сбрасывает его. Заметьте что Thread.interrupted() - статический метод класса Thread, и его вызов возвращает значение флага прерывания того потока, из которого он был вызван. Поэтому этот метод вызывается только изнутри потока и позволяет потоку проверить своё состояние прерывания.

Итак, вернемся к нашей программе. Механизм прерывания позволит нам решить проблему с засыпанием потока. У методов, приостанавливающих выполнение потока, таких как sleep(), wait() и join() есть одна особенность - если во время их выполнения будет вызван метод interrupt() этого потока, они, не дожидаясь конца времени ожидания, сгенерируют исключение InterruptedException.

Переделаем программу Incremenator – теперь вместо завершения потока с помощью метода finish() будем использовать стандартный метод interrupt(). А вместо проверки флага mFinish будем вызывать метод bool Thread.interrupted();
Так будет выглядеть класс Incremenator после добавления поддержки прерываний:

class Incremenator extends Thread { private volatile boolean mIsIncrement = true ; public void changeAction () //Меняет действие на противоположное { mIsIncrement = !mIsIncrement; } @Override public void run () { do { if (!Thread.interrupted()) //Проверка прерывания { if (mIsIncrement) Program.mValue++; //Инкремент else Program.mValue--; //Декремент //Вывод текущего значения переменной System.out.print(Program.mValue + " " ); } else return ; //Завершение потока try { Thread.sleep(1000 ); //Приостановка потока на 1 сек. }catch (InterruptedException e){ return ; //Завершение потока после прерывания } } while (true ); } } class Program { //Переменая, которой оперирует инкременатор public static int mValue = 0 ; static Incremenator mInc; //Объект побочного потока public static void main (String args) { mInc = new Incremenator(); //Создание потока System.out.print("Значение = " ); mInc.start(); //Запуск потока //Троекратное изменение действия инкременатора //с интервалом в i*2 секунд for (int i = 1 ; i <= 3 ; i++) { try { Thread.sleep(i*2 *1000 ); //Ожидание в течении i*2 сек. }catch (InterruptedException e){} mInc.changeAction(); //Переключение действия } mInc.interrupt(); //Прерывание побочного потока } } Консоль: Значение = 1 2 1 0 -1 -2 -1 0 1 2 3 4

Как видите, мы избавились от метода finish() и реализовали тот же механизм завершения потока с помощью встроенной системы прерываний. В этой реализации мы получили одно преимущество - метод sleep() вернет управление (сгенерирует исключение) незамедлительно после прерывания потока.

Заметьте что методы sleep() и join() обёрнуты в конструкции try-catch. Это необходимое условие работы этих методов. Вызывающий их код должен перехватывать исключение InterruptedException, которое они бросают при прерывании во время ожидания.

С запуском и завершением потоков разобрались, дальше я расскажу о методах, использующихся при работе с потоками.

Метод Thread.sleep()

Thread.sleep() - статический метод класса Thread, который приостанавливает выполнение потока, в котором он был вызван. Во время выполнения метода sleep() система перестает выделять потоку процессорное время, распределяя его между другими потоками. Метод sleep() может выполняться либо заданное кол-во времени (миллисекунды или наносекунды) либо до тех пор пока он не будет остановлен прерыванием (в этом случае он сгенерирует исключение InterruptedException).

Thread.sleep(1500 ); //Ждет полторы секунды Thread.sleep(2000 , 100 ); //Ждет 2 секунды и 100 наносекунд

Несмотря на то, что метод sleep() может принимать в качестве времени ожидания наносекунды, не стоит принимать это всерьез. Во многих системах время ожидания все равно округляется до миллисекунд а то и до их десятков.

Метод yield()

Статический метод Thread.yield() заставляет процессор переключиться на обработку других потоков системы. Метод может быть полезным, например, когда поток ожидает наступления какого-либо события и необходимо чтобы проверка его наступления происходила как можно чаще. В этом случае можно поместить проверку события и метод Thread.yield() в цикл:

//Ожидание поступления сообщения while (!msgQueue.hasMessages()) //Пока в очереди нет сообщений { Thread.yield(); //Передать управление другим потокам }

Метод join()

В Java предусмотрен механизм, позволяющий одному потоку ждать завершения выполнения другого. Для этого используется метод join(). Например, чтобы главный поток подождал завершения побочного потока myThready, необходимо выполнить инструкцию myThready.join() в главном потоке. Как только поток myThready завершится, метод join() вернет управление, и главный поток сможет продолжить выполнение.

Метод join() имеет перегруженную версию, которая получает в качестве параметра время ожидания. В этом случае join() возвращает управление либо когда завершится ожидаемый поток, либо когда закончится время ожидания. Подобно методу Thread.sleep() метод join может ждать в течение миллисекунд и наносекунд – аргументы те же.

С помощью задания времени ожидания потока можно, например, выполнять обновление анимированной картинки пока главный (или любой другой) поток ждёт завершения побочного потока, выполняющего ресурсоёмкие операции:

Thinker brain = new Thinker(); //Thinker - потомок класса Thread. brain.start(); //Начать "обдумывание". do { mThinkIndicator.refresh(); //mThinkIndicator - анимированная картинка. try { brain.join(250 ); //Подождать окончания мысли четверть секунды. }catch (InterruptedException e){} } while (brain.isAlive()); //Пока brain думает... //brain закончил думать (звучат овации).

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

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

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

Работать с приоритетами потока можно с помощью двух функций:

void setPriority(int priority) – устанавливает приоритет потока.
Возможные значения priority - MIN_PRIORITY, NORM_PRIORITY и MAX_PRIORITY.

int getPriority() – получает приоритет потока.

Некоторые полезные методы класса Thread

Это практически всё. Напоследок приведу несколько полезных методов работы с потоками.

boolean isAlive() - возвращает true если myThready() выполняется и false если поток еще не был запущен или был завершен.

setName(String threadName) – Задает имя потока.
String getName() – Получает имя потока.
Имя потока – ассоциированная с ним строка, которая в некоторых случаях помогает понять, какой поток выполняет некоторое действие. Иногда это бывает полезным.

static Thread Thread.currentThread() - статический метод, возвращающий объект потока, в котором он был вызван.

long getId() – возвращает идентификатор потока. Идентификатор – уникальное число, присвоенное потоку.

Заключение

Отмечу, что в статье рассказано далеко не про все нюансы многопоточного программирования. И коду, приведенному в примерах, для полной корректности не хватает некоторых нюансов. В частности, в примерах не используется синхронизация. Синхронизация потоков - тема, не изучив которую, программировать правильные многопоточные приложения не получится. Почитать о ней вы можете, например, в книге «Java Concurrency in Practice» или (всё на английском).

В статье были рассмотрены основные средства работы с потоками в Java. Если эта статья окажется полезной, то в следующей я расскажу о проблемах совместного доступа потоков к ресурсам и о методах их решения.

Здравствуйте! В этой статье я вкратце расскажу вам о процессах, потоках, и об основах многопоточного программирования на языке Java.
Наиболее очевидная область применения многопоточности – это программирование интерфейсов. Многопоточность незаменима тогда, когда необходимо, чтобы графический интерфейс продолжал отзываться на действия пользователя во время выполнения некоторой обработки информации. Например, поток, отвечающий за интерфейс, может ждать завершения другого потока, загружающего файл из интернета, и в это время выводить некоторую анимацию или обновлять прогресс-бар. Кроме того он может остановить поток загружающий файл, если была нажата кнопка «отмена».

Еще одна популярная и, пожалуй, одна из самых хардкорных областей применения многопоточности – игры. В играх различные потоки могут отвечать за работу с сетью, анимацию, расчет физики и т.п.

Давайте начнем. Сначала о процессах.

Процессы

Процесс - это совокупность кода и данных, разделяющих общее виртуальное адресное пространство. Чаще всего одна программа состоит из одного процесса, но бывают и исключения (например, браузер Chrome создает отдельный процесс для каждой вкладки, что дает ему некоторые преимущества, вроде независимости вкладок друг от друга). Процессы изолированы друг от друга, поэтому прямой доступ к памяти чужого процесса невозможен (взаимодействие между процессами осуществляется с помощью специальных средств).

Для каждого процесса ОС создает так называемое «виртуальное адресное пространство», к которому процесс имеет прямой доступ. Это пространство принадлежит процессу, содержит только его данные и находится в полном его распоряжении. Операционная система же отвечает за то, как виртуальное пространство процесса проецируется на физическую память.

Схема этого взаимодействия представлена на картинке. Операционная система оперирует так называемыми страницами памяти, которые представляют собой просто область определенного фиксированного размера. Если процессу становится недостаточно памяти, система выделяет ему дополнительные страницы из физической памяти. Страницы виртуальной памяти могут проецироваться на физическую память в произвольном порядке.

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

Потоки

Один поток – это одна единица исполнения кода. Каждый поток последовательно выполняет инструкции процесса, которому он принадлежит, параллельно с другими потоками этого процесса.

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

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

Вот как это выглядит:

Цветные квадраты на рисунке – это инструкции процессора (зеленые – инструкции главного потока, синие – побочного). Выполнение идет слева направо. После запуска побочного потока его инструкции начинают выполняться вперемешку с инструкциями главного потока. Кол-во выполняемых инструкций за каждый подход не определено.

То, что инструкции параллельных потоков выполняются вперемешку, в некоторых случаях может привести к конфликтам доступа к данным. Проблемам взаимодействия потоков будет посвящена следующая статья, а пока о том, как запускаются потоки в Java…

Запуск потоков

Каждый процесс имеет хотя бы один выполняющийся поток. Тот поток, с которого начинается выполнение программы, называется главным. В языке Java, после создания процесса, выполнение главного потока начинается с метода main(). Затем, по мере необходимости, в заданных программистом местах, и при выполнении заданных им же условий, запускаются другие, побочные потоки.

В языке Java поток представляется в виде объекта-потомка класса Thread. Этот класс инкапсулирует стандартные механизмы работы с потоком.

Запустить новый поток можно двумя способами:

Способ 1
Создать объект класса Thread, передав ему в конструкторе нечто, реализующее интерфейс Runnable. Этот интерфейс содержит метод run(), который будет выполняться в новом потоке. Поток закончит выполнение, когда завершится его метод run().

Выглядит это так:

Class SomeThing //Нечто, реализующее интерфейс Runnable implements Runnable //(содержащее метод run()) { public void run() //Этот метод будет выполняться в побочном потоке { System.out.println("Привет из побочного потока!"); } } public class Program //Класс с методом main() { static SomeThing mThing; //mThing - объект класса, реализующего интерфейс Runnable public static void main(String args) { mThing = new SomeThing(); Thread myThready = new Thread(mThing); //Создание потока "myThready" myThready.start(); //Запуск потока System.out.println("Главный поток завершён..."); } }

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

Public class Program //Класс с методом main(). { public static void main(String args) { //Создание потока Thread myThready = new Thread(new Runnable() { public void run() //Этот метод будет выполняться в побочном потоке { System.out.println("Привет из побочного потока!"); } }); myThready.start(); //Запуск потока System.out.println("Главный поток завершён..."); } }

Способ 2
Создать потомка класса Thread и переопределить его метод run():

Class AffableThread extends Thread { @Override public void run() //Этот метод будет выполнен в побочном потоке { System.out.println("Привет из побочного потока!"); } } public class Program { static AffableThread mSecondThread; public static void main(String args) { mSecondThread = new AffableThread(); //Создание потока mSecondThread.start(); //Запуск потока System.out.println("Главный поток завершён..."); } }

В приведённом выше примере в методе main() создается и запускается еще один поток. Важно отметить, что после вызова метода mSecondThread.start() главный поток продолжает своё выполнение, не дожидаясь пока порожденный им поток завершится. И те инструкции, которые идут после вызова метода start(), будут выполнены параллельно с инструкциями потока mSecondThread.

Для демонстрации параллельной работы потоков давайте рассмотрим программу, в которой два потока спорят на предмет философского вопроса «что было раньше, яйцо или курица?». Главный поток уверен, что первой была курица, о чем он и будет сообщать каждую секунду. Второй же поток раз в секунду будет опровергать своего оппонента. Всего спор продлится 5 секунд. Победит тот поток, который последним изречет свой ответ на этот, без сомнения, животрепещущий философский вопрос. В примере используются средства, о которых пока не было сказано (isAlive() sleep() и join()). К ним даны комментарии, а более подробно они будут разобраны дальше.

Class EggVoice extends Thread { @Override public void run() { for(int i = 0; i < 5; i++) { try{ sleep(1000); //Приостанавливает поток на 1 секунду }catch(InterruptedException e){} System.out.println("яйцо!"); } //Слово «яйцо» сказано 5 раз } } public class ChickenVoice //Класс с методом main() { static EggVoice mAnotherOpinion; //Побочный поток public static void main(String args) { mAnotherOpinion = new EggVoice(); //Создание потока System.out.println("Спор начат..."); mAnotherOpinion.start(); //Запуск потока for(int i = 0; i < 5; i++) { try{ Thread.sleep(1000); //Приостанавливает поток на 1 секунду }catch(InterruptedException e){} System.out.println("курица!"); } //Слово «курица» сказано 5 раз if(mAnotherOpinion.isAlive()) //Если оппонент еще не сказал последнее слово { try{ mAnotherOpinion.join(); //Подождать пока оппонент закончит высказываться. }catch(InterruptedException e){} System.out.println("Первым появилось яйцо!"); } else //если оппонент уже закончил высказываться { System.out.println("Первой появилась курица!"); } System.out.println("Спор закончен!"); } } Консоль: Спор начат... курица! яйцо! яйцо! курица! яйцо! курица! яйцо! курица! яйцо! курица! Первой появилась курица! Спор закончен!

В приведенном примере два потока параллельно в течении 5 секунд выводят информацию на консоль. Точно предсказать, какой поток закончит высказываться последним, невозможно. Можно попытаться, и можно даже угадать, но есть большая вероятность того, что та же программа при следующем запуске будет иметь другого «победителя». Это происходит из-за так называемого «асинхронного выполнения кода». Асинхронность означает то, что нельзя утверждать, что какая-либо инструкция одного потока, выполнится раньше или позже инструкции другого. Или, другими словами, параллельные потоки независимы друг от друга, за исключением тех случаев, когда программист сам описывает зависимости между потоками с помощью предусмотренных для этого средств языка.

Теперь немного о завершении процессов…

Завершение процесса и демоны

В Java процесс завершается тогда, когда завершается последний его поток. Даже если метод main() уже завершился, но еще выполняются порожденные им потоки, система будет ждать их завершения.

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

Объявить поток демоном достаточно просто - нужно перед запуском потока вызвать его метод setDaemon(true) ;
Проверить, является ли поток демоном, можно вызвав его метод boolean isDaemon() ;

Завершение потоков

В Java существуют (существовали) средства для принудительного завершения потока. В частности метод Thread.stop() завершает поток незамедлительно после своего выполнения. Однако этот метод, а также Thread.suspend(), приостанавливающий поток, и Thread.resume(), продолжающий выполнение потока, были объявлены устаревшими и их использование отныне крайне нежелательно. Дело в том что поток может быть «убит» во время выполнения операции, обрыв которой на полуслове оставит некоторый объект в неправильном состоянии, что приведет к появлению трудноотлавливаемой и случайным образом возникающей ошибке.

Вместо принудительного завершения потока применяется схема, в которой каждый поток сам ответственен за своё завершение. Поток может остановиться либо тогда, когда он закончит выполнение метода run(), (main() - для главного потока) либо по сигналу из другого потока. Причем как реагировать на такой сигнал - дело, опять же, самого потока. Получив его, поток может выполнить некоторые операции и завершить выполнение, а может и вовсе его проигнорировать и продолжить выполняться. Описание реакции на сигнал завершения потока лежит на плечах программиста.

Java имеет встроенный механизм оповещения потока, который называется Interruption (прерывание, вмешательство), и скоро мы его рассмотрим, но сначала посмотрите на следующую программку:

Incremenator - поток, который каждую секунду прибавляет или вычитает единицу из значения статической переменной Program.mValue. Incremenator содержит два закрытых поля – mIsIncrement и mFinish. То, какое действие выполняется, определяется булевой переменной mIsIncrement - если оно равно true, то выполняется прибавление единицы, иначе - вычитание. А завершение потока происходит, когда значение mFinish становится равно true.

Class Incremenator extends Thread { //О ключевом слове volatile - чуть ниже private volatile boolean mIsIncrement = true; private volatile boolean mFinish = false; public void changeAction() //Меняет действие на противоположное { mIsIncrement = !mIsIncrement; } public void finish() //Инициирует завершение потока { mFinish = true; } @Override public void run() { do { if(!mFinish) //Проверка на необходимость завершения { if(mIsIncrement) Program.mValue++; //Инкремент else Program.mValue--; //Декремент //Вывод текущего значения переменной System.out.print(Program.mValue + " "); } else return; //Завершение потока try{ Thread.sleep(1000); //Приостановка потока на 1 сек. }catch(InterruptedException e){} } while(true); } } public class Program { //Переменая, которой оперирует инкременатор public static int mValue = 0; static Incremenator mInc; //Объект побочного потока public static void main(String args) { mInc = new Incremenator(); //Создание потока System.out.print("Значение = "); mInc.start(); //Запуск потока //Троекратное изменение действия инкременатора //с интервалом в i*2 секунд for(int i = 1; i <= 3; i++) { try{ Thread.sleep(i*2*1000); //Ожидание в течении i*2 сек. }catch(InterruptedException e){} mInc.changeAction(); //Переключение действия } mInc.finish(); //Инициация завершения побочного потока } } Консоль: Значение = 1 2 1 0 -1 -2 -1 0 1 2 3 4

Взаимодействовать с потоком можно с помощью метода changeAction() (для смены вычитания на сложение и наоборот) и метода finish() (для завершения потока).

В объявлении переменных mIsIncrement и mFinish было использовано ключевое слово volatile (изменчивый, не постоянный). Его необходимо использовать для переменных, которые используются разными потоками. Это связано с тем, что значение переменной, объявленной без volatile, может кэшироваться отдельно для каждого потока, и значение из этого кэша может различаться для каждого из них. Объявление переменной с ключевым словом volatile отключает для неё такое кэширование и все запросы к переменной будут направляться непосредственно в память.

В этом примере показано, каким образом можно организовать взаимодействие между потоками. Однако есть одна проблема при таком подходе к завершению потока - Incremenator проверяет значение поля mFinish раз в секунду, поэтому может пройти до секунды времени между тем, когда будет выполнен метод finish(), и фактическим завершения потока. Было бы замечательно, если бы при получении сигнала извне, метод sleep() возвращал выполнение и поток незамедлительно начинал своё завершение. Для выполнения такого сценария существует встроенное средство оповещения потока, которое называется Interruption (прерывание, вмешательство).

Interruption

Класс Thread содержит в себе скрытое булево поле, подобное полю mFinish в программе Incremenator, которое называется флагом прерывания. Установить этот флаг можно вызвав метод interrupt() потока. Проверить же, установлен ли этот флаг, можно двумя способами. Первый способ - вызвать метод bool isInterrupted() объекта потока, второй - вызвать статический метод bool Thread.interrupted(). Первый метод возвращает состояние флага прерывания и оставляет этот флаг нетронутым. Второй метод возвращает состояние флага и сбрасывает его. Заметьте что Thread.interrupted() - статический метод класса Thread, и его вызов возвращает значение флага прерывания того потока, из которого он был вызван. Поэтому этот метод вызывается только изнутри потока и позволяет потоку проверить своё состояние прерывания.

Итак, вернемся к нашей программе. Механизм прерывания позволит нам решить проблему с засыпанием потока. У методов, приостанавливающих выполнение потока, таких как sleep(), wait() и join() есть одна особенность - если во время их выполнения будет вызван метод interrupt() этого потока, они, не дожидаясь конца времени ожидания, сгенерируют исключение InterruptedException.

Переделаем программу Incremenator – теперь вместо завершения потока с помощью метода finish() будем использовать стандартный метод interrupt(). А вместо проверки флага mFinish будем вызывать метод bool Thread.interrupted();
Так будет выглядеть класс Incremenator после добавления поддержки прерываний:

Class Incremenator extends Thread { private volatile boolean mIsIncrement = true; public void changeAction() //Меняет действие на противоположное { mIsIncrement = !mIsIncrement; } @Override public void run() { do { if(!Thread.interrupted()) //Проверка прерывания { if(mIsIncrement) Program.mValue++; //Инкремент else Program.mValue--; //Декремент //Вывод текущего значения переменной System.out.print(Program.mValue + " "); } else return; //Завершение потока try{ Thread.sleep(1000); //Приостановка потока на 1 сек. }catch(InterruptedException e){ return; //Завершение потока после прерывания } } while(true); } } class Program { //Переменая, которой оперирует инкременатор public static int mValue = 0; static Incremenator mInc; //Объект побочного потока public static void main(String args) { mInc = new Incremenator(); //Создание потока System.out.print("Значение = "); mInc.start(); //Запуск потока //Троекратное изменение действия инкременатора //с интервалом в i*2 секунд for(int i = 1; i <= 3; i++) { try{ Thread.sleep(i*2*1000); //Ожидание в течении i*2 сек. }catch(InterruptedException e){} mInc.changeAction(); //Переключение действия } mInc.interrupt(); //Прерывание побочного потока } } Консоль: Значение = 1 2 1 0 -1 -2 -1 0 1 2 3 4

Как видите, мы избавились от метода finish() и реализовали тот же механизм завершения потока с помощью встроенной системы прерываний. В этой реализации мы получили одно преимущество - метод sleep() вернет управление (сгенерирует исключение) незамедлительно после прерывания потока.

Заметьте что методы sleep() и join() обёрнуты в конструкции try-catch. Это необходимое условие работы этих методов. Вызывающий их код должен перехватывать исключение InterruptedException, которое они бросают при прерывании во время ожидания.

С запуском и завершением потоков разобрались, дальше я расскажу о методах, использующихся при работе с потоками.

Метод Thread.sleep()

Thread.sleep() - статический метод класса Thread, который приостанавливает выполнение потока, в котором он был вызван. Во время выполнения метода sleep() система перестает выделять потоку процессорное время, распределяя его между другими потоками. Метод sleep() может выполняться либо заданное кол-во времени (миллисекунды или наносекунды) либо до тех пор пока он не будет остановлен прерыванием (в этом случае он сгенерирует исключение InterruptedException).

Thread.sleep(1500); //Ждет полторы секунды Thread.sleep(2000, 100); //Ждет 2 секунды и 100 наносекунд

Несмотря на то, что метод sleep() может принимать в качестве времени ожидания наносекунды, не стоит принимать это всерьез. Во многих системах время ожидания все равно округляется до миллисекунд а то и до их десятков.

Метод yield()

Статический метод Thread.yield() заставляет процессор переключиться на обработку других потоков системы. Метод может быть полезным, например, когда поток ожидает наступления какого-либо события и необходимо чтобы проверка его наступления происходила как можно чаще. В этом случае можно поместить проверку события и метод Thread.yield() в цикл:

//Ожидание поступления сообщения while(!msgQueue.hasMessages()) //Пока в очереди нет сообщений { Thread.yield(); //Передать управление другим потокам }

Метод join()

В Java предусмотрен механизм, позволяющий одному потоку ждать завершения выполнения другого. Для этого используется метод join(). Например, чтобы главный поток подождал завершения побочного потока myThready, необходимо выполнить инструкцию myThready.join() в главном потоке. Как только поток myThready завершится, метод join() вернет управление, и главный поток сможет продолжить выполнение.

Метод join() имеет перегруженную версию, которая получает в качестве параметра время ожидания. В этом случае join() возвращает управление либо когда завершится ожидаемый поток, либо когда закончится время ожидания. Подобно методу Thread.sleep() метод join может ждать в течение миллисекунд и наносекунд – аргументы те же.

С помощью задания времени ожидания потока можно, например, выполнять обновление анимированной картинки пока главный (или любой другой) поток ждёт завершения побочного потока, выполняющего ресурсоёмкие операции:

Thinker brain = new Thinker(); //Thinker - потомок класса Thread. brain.start(); //Начать "обдумывание". do { mThinkIndicator.refresh(); //mThinkIndicator - анимированная картинка. try{ brain.join(250); //Подождать окончания мысли четверть секунды. }catch(InterruptedException e){} } while(brain.isAlive()); //Пока brain думает... //brain закончил думать (звучат овации).

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

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

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

Работать с приоритетами потока можно с помощью двух функций:

void setPriority(int priority) – устанавливает приоритет потока.
Возможные значения priority - MIN_PRIORITY, NORM_PRIORITY и MAX_PRIORITY.

int getPriority() – получает приоритет потока.

Некоторые полезные методы класса Thread

Это практически всё. Напоследок приведу несколько полезных методов работы с потоками.

boolean isAlive() - возвращает true если myThready() выполняется и false если поток еще не был запущен или был завершен.

setName(String threadName) – Задает имя потока.
String getName() – Получает имя потока.
Имя потока – ассоциированная с ним строка, которая в некоторых случаях помогает понять, какой поток выполняет некоторое действие. Иногда это бывает полезным.

static Thread Thread.currentThread() - статический метод, возвращающий объект потока, в котором он был вызван.

long getId() – возвращает идентификатор потока. Идентификатор – уникальное число, присвоенное потоку.

Заключение

Отмечу, что в статье рассказано далеко не про все нюансы многопоточного программирования. И коду, приведенному в примерах, для полной корректности не хватает некоторых нюансов. В частности, в примерах не используется синхронизация. Синхронизация потоков - тема, не изучив которую, программировать правильные многопоточные приложения не получится. Почитать о ней вы можете, например, в книге «Java Concurrency in Practice» или

Немного о принципе многопоточности

Многопоточность - это принцип построения программы, при котором несколько блоков могут выполняться одновременно и не мешать друг другу. Предположим, нам необходимо, чтобы какие-то операции выполнялись одновременно.
Например, у нас существует два объекта, выполняющие конкретные операции независимо друг от друга. Но они используют один и тот же метод, который добавляет к статической переменной число, передаваемое методу в параметре. Однако если эти операции будут выполняться одновременно, то непонятно будет, что запишется в этой переменной, ведь мы одновременно прибавляем к исходному значению переменной два разных числа.

Возникнет странная ситуация. Для того чтобы такого не происходило, для этого метода необходимо установить так называемый признак блокировки. Если какой-либо объект начинает выполнять метод, то происходит блокировка этого метода, и никто больше не может его выполнить, пока данный объект не закончит работу с ним. Все другие объекты, которые должны выполнить этот метод, будут находиться в состоянии ожидания.

Создание потоков на основе класса Thread

Чтобы создать свой поток, необходимо создать дочерний класс класса Thread. Давайте создадим такой класс и затем разберем весь текст программы (листинг 5.7).

Листинг 5.7.
Создание потока на основе класса Thread

Public class MyThread extends Thread{ private int seconds; public MyThread(int seconds) { this.seconds = seconds; } public void run() { try { for (int i = 0; i < this.seconds; i++) { Thread.sleep(1000); System.out.println("Идет секунда: " + i); } } catch (InterruptedException e) { System.out.println("У нас проблемы с потоком"); } } public static void main(String args) { MyThread myThread = new MyThread(5); myThread.start(); new MyThread(15); } }

Вначале мы создаем дочерний класс для класса Thread, затем - конструктор с параметром, который установит количество секунд выполнения потока. После этого мы определяем метод run. который начинает действовать при запуске потока.
Чтобы его вызвать, необходимо вызвать метод start () - «стартовать поток».

У класса Thread есть метод sleep, который позволяет приостановить выполнение потока на заданное количество миллисекунд (внимание: отсчет идет в миллисекундах). Чтобы использовать данный метод, необходимо поместить его в блок try, а блок catch сделать обрабатывающим исключение InterruptedException. Затем мы запускаем два потока, которые будут выполняться одновременно. Чтобы их различить, можно переделать код так (листинг 5.8).

Листинг 5.8.
Создание двух потоков, которые будут выполняться в классе Thread одновременно

Public class MyThread extends Thread{ private int seconds; private static int numbers = 0; private int number; public static int setNumber() { return ++numbers; } public MyThread(int seconds) { this.seconds = seconds; number = MyThread.setNumber(); } public void run() { try { for (int i = 0; i < this.seconds; i++) { Thread.sleep(1000); System.out.println("Идет секунда: " + i + " выполнения потока под номером " + this.number); } } catch (InterruptedException e) { System.out.println("У нас проблемы с потоком"); } } public static void main(String args) { MyThread myThread = new MyThread(5); myThread.start(); new MyThread(15).start(); } }

Теперь мы каждому потоку присвоили свой уникальный номер.

Использование интерфейса Runnable

Можно создавать классы потоков, реализуя интерфейс Runnable. Это бывает порой значительно удобнее. Например, код из предыдущего примера можно переписать так (листинг 5.9).

Листинг 5.9.
Создание классов потоков с использованием интерфейса Runnable

В некоторых случаях так представляется удобней: обычно у подобного создания потоков более короткая запись.
И в том и в другом случае потоку можно присвоить имя. Обычно оно задается в конструкторе в качестве последнего параметра.



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: