Как отправить метод в отдельный поток java. Многопоточное программирование

должны воспользоваться классом java.lang.Thread. В этом классе определены все методы, необходимые для создания потоков, управления их состоянием и синхронизации.

Как пользоваться классом Thread?

Есть две возможности.

  • Во-первых, вы можете создать свой дочерний класс на базе класса Thread. При этом вы должны переопределить метод run. Ваша реализация этого метода будет работать в рамках отдельного потока.
  • Во-вторых, ваш класс может реализовать интерфейс Runnable. При этом в рамках вашего класса необходимо определить метод run, который будет работать как отдельный поток.

Второй способ особенно удобен в тех случаях, когда ваш класс должен быть унаследован от какого-либо другого класса (например, от класса Applet) и при этом вам нужна многопоточность. Так как в языке программирования Java нет множественного наследования, невозможно создать класс, для которого в качестве родительского будут выступать классы Applet и Thread. В этом случае реализация интерфейса Runnable является единственным способом решения задачи.

Методы класса Thread

В классе Thread определены три поля, несколько конструкторов и большое количество методов, предназначенных для работы с потоками. Ниже мы привели краткое описание полей, конструкторов и методов.

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

Методы класса Thread предоставляют все необходимые возможности для управления потоками, в том числе для их синхронизации.

Поля

Три статических поля предназначены для назначения приоритетов потокам.

  • NORM_PRIORITY

Нормальный

public final static int NORM_PRIORITY;
  • MAX_PRIORITY

Максимальный

public final static int MAX_PRIORITY;
  • MIN_PRIORITY

Минимальный

public final static int MIN_PRIORITY;

Конструкторы

Создание нового объекта Thread

public Thread();

Создвание нового объекта Thread с указанием объекта, для которого будет вызываться метод run

public Thread(Runnable target); public Thread(Runnable target, String name);

Создание объекта Thread с указанием его имени

public Thread(String name);

Создание нового объекта Thread с указанием группы потока и объекта, для которого вызывается метод run

public Thread(ThreadGroup group, Runnable target);

Аналогично предыдущему, но дополнительно задается имя нового объекта Thread

public Thread(ThreadGroup group, Runnable target, String name);

Создание нового объекта Thread с указанием группы потока и имени объекта

public Thread(ThreadGroup group, String name);

Методы

  • activeCount

Текущее количество активных потоков в группе, к которой принадлежит поток

public static int activeCount();
  • checkAccess

Текущему потоку разрешается изменять объект Thread

public void checkAccesss();
  • countStackFrames

Определение количества фреймов в стеке

public int countStackFrames();
  • currentThread

Определение текущего работающего потока

public static Thread currentThread();
  • destroy

Принудительное завершение работы потока

public void destroy();
  • dumpStack

Вывод текущего содержимого стека для отладки

public static void dumpStack();
  • enumerate

Получение всех объектов Tread данной группы

public static int enumerate(Thread tarray);
  • getName

Определение имени потока

public final String getName();
  • getPriority

Определение текущего приоритета потока

public final int getPriority();
  • getThreadGroup

Определение группы, к которой принадлежит поток

public final ThreadGroup getThreadGroup();
  • interrupt

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

public void interrupt();
  • interrupted
public static boolean interrupted();
  • isAlive

Определение, выполняется поток или нет

public final boolean isAlive();
  • isDaemon

Определение, является ли поток демоном

public final boolean isDaemon();
  • isInterrupted

Определение, является ли поток прерванным

public boolean isInterrupted();
  • join

Ожидание завершения потока

public final void join();

Ожидание завершения потока в течение заданного времени. Время задается в миллисекундах

public final void join(long millis);

Ожидание завершения потока в течение заданного времени. Время задается в миллисекундах и наносекундах

public final void join(long millis, int nanos);
  • resume

Запуск временно приостановленного потока

public final void resume();

Метод вызывается в том случае, если поток был создан как объект с интерфейсом Runnable

public void run();
  • setDaemon

Установка для потока режима демона

public final void setDaemon(boolean on);
  • setName

Устаовка имени потока

public final void setName(String name);
  • setPriority

Установка приоритета потока

public final void setPriority(int newPriority);
  • sleep
public static void sleep(long millis);

Задержка потока на заднное время. Время задается в миллисекундах и наносекундах

public static void sleep(long millis, int nanos);
  • start

Запуск потока на выполнение

public void start();
  • stop

Остановка выполнения потока

public final void stop();

Аварийная остановка выполнения потока с заданным исключением

public final void stop(Throwable obj);
  • suspend

Приостановка потока

public final void suspend();
  • toString

Строка, представляющая объект-поток

public String toString();
  • yield

Приостановка текущего потока для того чтобы управление было передано другому потоку

public static void yield();

Создание дочернего класса на базе класса Thread

Рассмотрим первый способ реализации многопоточности, основанный на наследовании от класса Thread. При использовании этого способа вы определяете для потока отдельный класс, например, так:

class DrawRectangles extends Thread { . . . public void run() { . . . } }

Здесь определен класс DrawRectangles, который является дочерним по отношению к классу Thread.

Обратите внимание на метод run. Создавая свой класс на базе класса Thread, вы должны всегда определять этот метод, который и будет выполняться в рамках отдельного потока.

Заметим, что метод run не вызывается напрямую никакими другими методами. Он получает управление при запуске потока методом start.

Как это происходит?

Рассмотрим процедуру запуска потока на примере некоторого класса DrawRectangles.

Вначале ваше приложение должно создать объект класса Thread:

public class MultiTask2 extends Applet { Thread m_DrawRectThread = null; . . . public void start() { if (m_DrawRectThread == null) { m_DrawRectThread = new DrawRectangles(this); m_DrawRectThread.start(); } } }

Создание объекта выполняется оператором new в методе start, который получает управление, когда пользователь открывает документ HTML с аплетом. Сразу после создания поток запускается на выполнение, для чего вызывается метод start.

Что касается метода run, то если поток используется для выполнения какой либо периодической работы, то этот метод содержит внутри себя бесконечный цикл. Когда цикл завершается и метод run возвращает управление, поток прекращает свою работу нормальным, не аварийным образом. Для аварийного завершения потока можно использовать метод interrupt.

Остановка работающего потока выполняется методом stop. Обычно остановка всех работающих потоков, созданных аплетом, выполняется методом stop класса аплета:

public void stop() { if (m_DrawRectThread != null) { m_DrawRectThread.stop(); m_DrawRectThread = null; } }

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

Реализация интерфейса Runnable

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

Идея заключается в том, что основной класс аплета, который является дочерним по отношению к классу Applet, дополнительно реализует интерфейс Runnable, как это показано ниже:

public class MultiTask extends Applet implements Runnable { Thread m_MultiTask = null; . . . public void run() { . . . } public void start() { if (m_MultiTask == null) { m_MultiTask = new Thread(this); m_MultiTask.start(); } } public void stop() { if (m_MultiTask != null) { m_MultiTask.stop(); m_MultiTask = null; } } }

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

Для создания потока используется оператор new. Поток создается как объект класса Thread, причем конструктору передается ссылка на класс аплета:

m_MultiTask = new Thread(this);

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

Как запустить поток?

Запуск выполняется, как и раньше, методом start. Обычно поток запускается из метода start аплета, когда пользователь отображает страницу сервера Web, содержащую аплет. Остановка потока выполняется методом stop.

Здравствуйте! В этой статье я вкратце расскажу вам о процессах, потоках, и об основах многопоточного программирования на языке 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» или Прежде, чем узнать про потоки Java, давайте заглянем в недалёкое будущее. Представьте, что вы подали резюме и прошли собеседование. Вас и пару дюжин будущих коллег пригласили на работу в большую Software-компанию. Среди прочих хлопот нужно подать бумажные документы для трудоустройства уставшему сотруднику HR-отдела.

Чтобы ускорить процесс, претендентов на должность можно разделить на две группы и распределить их между двумя HR-менеджерами (если таковые есть в компании). В результате мы получаем ускорение процесса за счёт параллельной (parallel ) работы по оформлению.

Если же кадровик в компании один, то придётся как-то выкручиваться. Например, можно снова- таки разбить всех на две группы, например, собеседовать поочерёдно девушек и юношей.

Или по другому принципу: так как в нижней группе больше народа, будем чередовать на одного юношу двух девушек.

Такой способ организации работы называется многопоточным . Наш утомлённый кадровик переключается на разные группы для оформления из них очередного сотрудника. Групп, может быть, одиннадцать, а кадровиков – четыре. В этом случае многопоточная (multithreading ) обработка будет происходить параллельно несколькими HR-ами, которые могут брать очередного человека из любой группы для обработки его документов.

Процессы

Процессом (process ) в данном случае будет организация работы приёма документов. В организации можно выделить несколько процессов: бухгалтерский учёт, разработка ПО, встречи с клиентами, работа склада и т. д. На каждый процесс выделены ресурсы: помещение, сотрудники для его исполнения. Процессы изолированы друг от друга: у кадровиков отсутствует доступ в бухгалтерскую базу, а менеджеры по работе с клиентами не бегают по складу. Если процесс должен получить доступ к чужим ресурсам, необходимо наладить межпроцессное взаимодействие: служебные записки, совместные совещания.

Потоки

Работа в процессе организована в виде потоков (java thread). Для отдела кадров, поток – это организация работы по обслуживанию группы. На самой первой картинке – один поток, последующих трёх – два. Внутри процесса потоки могут выполняться параллельно – два кадровика принимают две или более группы будущих сотрудников. Взаимодействие кадровиков с группами – обработку потоков внутри процесса – называют синхронизацией потоков . На рисунках оформления одним кадровиком двух групп видны показаны способы: равномерный (девушка – юноша – девушка – юноша) и с разными приоритетами (две девушки чередуются с одним юношей). Потоки имеют доступ к ресурсам процесса, к которому они относятся: группам к кадровику даны образцы бланков заявлений, ручки для заполнения документов. Но если потоки взаимодействуют с общими для них вещами – то возможны казусы. Если кадровик попросит крикнуть имя последнего человека в очереди – то, в случае с двумя группами, он не уверен заранее, что услышит женское имя или мужское. Подобные конфликты доступа к данным, блокировки и способы их разрешения – очень важная тема.

Состояния потока

Каждый поток пребывает в одном из следующих состояний (state):
  • Создан (New) – очередь к кадровику готовится, люди организуются.
  • Запущен (Runnable) – наша очередь выстроилась к кадровику и обрабатывается.
  • Заблокирован (Blocked) – последний в очереди юноша пытается выкрикнуть имя, но услышав, что девушка в соседней группе начала делать это раньше него, замолчал.
  • Завершён (Terminated) - вся очередь оформилась у кадровика и в ней нет необходимости.
  • Ожидает(Waiting) – одна очередь ждёт сигнала от другой.
Организация потоков и их взаимодействие – это основа эффективной работы процессов.

Вернемся в IT-мир

В XXI веке многопоточное и параллельное выполнение стало актуальным. С 90-х годов прошлого века многозадачные операционные системы Windows, MacOS и Linux прочно обосновались на домашних компьютерах. В них часто можно встретить четырёх- и более ядерные процессоры. Число параллельных блоков GPU-видеокарт уже перевалило за тысячу. Популярные программы пишутся с учетом многопоточности (multithreading), например, современные версии ПО обработки графики, видео или оперирующих большим объемом данных: Adobe Photoshop, WinRar, Mathematica, современные игры. Многопоточность Java – очень важная, востребованная и сложная тема. Поэтому в курсе JavaRush встречается много задач, чтобы разобраться с ней очень хорошо. Java-примеры на многопоточность помогут освоить основные нюансы и тонкости этой области, синхронизации работы потоков.

Процесс

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

Поток

Java Thread (поток). Иногда, чтобы не путать с другими классами Java – Stream и подобными, потоки Java часто переводят как нить. Они используют выделенные для процесса ресурсы и являются способом выполнения процесса. Главный поток выполняет метод main и завершается. При выполнении процесса могут порождаться дополнительные потоки (дочерние). Потоки одного процесса могут между собой обмениваться данными. Многопоточность Java требует учитывать синхронизацию данных, не забывайте об этом. В Java процесс завершается тогда, когда закончил работу последний его поток. Для фоновых задач поток можно запустить как демон (daemon), отличие которого от обычного в том, что они будут принудительно завершены при окончании работы всех не- daemon потоков процесса.

Первое многопоточное приложение

Существует более полудюжины способов создания потоков, в рамках JavaRush курса мы их подробно разберём. Для начала познакомимся с одним из базовых. Имеется специальный класс Thread в методе run() которого необходимо написать код, реализующий логику программы. После создания потока, можно запустить его, вызвав метод start() . Напишем демонстрационную программу, реализующую пример многопоточности Java. class PeopleQueue extends Thread { // Наша очередь из сотрудников, наследник класса Thread private String names; PeopleQueue (String. . . names) { // Конструктор, аргумент- массив имен сотрудников this . names = names; } @Override public void run () { // Этот метод будет вызван при старте потока for (int i = 0 ; i < names. length; i++ ) { // Вывод в цикле с паузой 0.5 сек очередного сотрудника System. out. println ("Обработаны документы: " + names[ i] ) ; try { sleep (500 ) ; // Задержка в 0.5 сек } catch (Exception e) { } } } } public class HR { // Класс для демонстрации работы потока public static void main (String args) { // Создаем две очереди PeopleQueue queue1 = new PeopleQueue ("Иван" , "Сергей" , "Николай" , "Фердинанд" , "Василий" ) ; PeopleQueue queue2 = new PeopleQueue ("Мария" , "Людмила" , "Алиса" , "Карина" , "Ольга" ) ; System. out. println ("Начали!" ) ; // Сообщение из главного потока программы queue1. start () ; //Запускаем одну очередь (дочерний поток) queue2. start () ; //Запускаем вторую (дочерний поток) } } Запустим программу. В консоли виден вывод сообщения главным потоком. Далее, каждый дочерний поток queue1 и queue2 поочередно выводят сообщения в общую для них консоль об очередном обработанном сотруднике. Один из возможных вариантов работы программы: Начали! Обработаны документы: Мария Обработаны документы: Иван Обработаны документы: Людмила Обработаны документы: Сергей Обработаны документы: Алиса Обработаны документы: Николай Обработаны документы: Карина Обработаны документы: Фердинанд Обработаны документы: Ольга Обработаны документы: Василий Process finished with exit code 0 Многопоточность в Java – тема трудная и многосторонняя. Умение писать код с использованием параллельных, многозадачных и многопоточных вычислений поможет вам эффективно реализовать задачи на современных многоядерных процессорах и кластерах, состоящих из множества компьютеров.

Многопоточное программирование позволяет разделить представление и обработку информации на несколько «легковесных» процессов (light-weight processes), имеющих общий доступ как к методам различных объектов приложения, так и к их полям. Многопоточность незаменима в тех случаях, когда графический интерфейс должен реагировать на действия пользователя при выполнении определенной обработки информации. Потоки могут взаимодействовать друг с другом через основной «родительский» поток, из которого они стартованы.

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

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

Преимущества потоков перед процессами

  • потоки намного легче процессов поскольку требуют меньше времени и ресурсов;
  • переключение контекста между потоками намного быстрее, чем между процессами;
  • намного проще добиться взаимодействия между потоками, чем между процессами.

Главный поток

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

Класс Thread

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

Конструкторы класса Thread

Thread(); Thread(Runnable target); Thread(Runnable target, String name); Thread(String name); Thread(ThreadGroup group, Runnable target); Thread(ThreadGroup group, Runnable target, String name); Thread(ThreadGroup group, String name);

  • target – экземпляр класса реализующего интерфейс Runnable;
  • name – имя создаваемого потока;
  • group – группа к которой относится поток.

Пример создания потока, который входит в группу, реализует интерфейс Runnable и имеет свое уникальное название:

Runnable r = new MyClassRunnable(); ThreadGroup tg = new ThreadGroup(); Thread t = new Thread(tg, r, "myThread");

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

Несмотря на то, что главный поток создаётся автоматически, им можно управлять. Для этого необходимо создать объект класса Thread вызовом метода currentThread() .

Методы класса Thread

Наиболее часто используемые методы класса Thread для управления потоками:

  • long getId() - получение идентификатора потока;
  • String getName() - получение имени потока;
  • int getPriority() - получение приоритета потока;
  • State getState() - определение состояния потока;
  • void interrupt() - прерывание выполнения потока;
  • boolean isAlive() - проверка, выполняется ли поток;
  • boolean isDaemon() - проверка, является ли поток «daemon»;
  • void join() - ожидание завершения потока;
  • void join(millis) - ожидание millis милисекунд завершения потока;
  • void notify() - «пробуждение» отдельного потока, ожидающего «сигнала»;
  • void notifyAll() - «пробуждение» всех потоков, ожидающих «сигнала»;
  • void run() - запуск потока, если поток был создан с использованием интерфейса Runnable;
  • void setDaemon(bool) - определение «daemon» потока;
  • void setPriority(int) - определение приоритета потока;
  • void sleep(int) - приостановка потока на заданное время;
  • void start() - запуск потока.
  • void wait() - приостановка потока, пока другой поток не вызовет метод notify();
  • void wait(millis) - приостановка потока на millis милисекунд или пока другой поток не вызовет метод notify();

Жизненный цикл потока

При выполнении программы объект Thread может находиться в одном из четырех основных состояний: «новый», «работоспособный», «неработоспособный» и «пассивный». При создании потока он получает состояние «новый» (NEW) и не выполняется. Для перевода потока из состояния «новый» в «работоспособный» (RUNNABLE) следует выполнить метод start(), вызывающий метод run().

Поток может находиться в одном из состояний, соответствующих элементам статически вложенного перечисления Thread.State:

NEW - поток создан, но еще не запущен;
RUNNABLE - поток выполняется;
BLOCKED - поток блокирован;
WAITING - поток ждет окончания работы другого потока;
TIMED_WAITING - поток некоторое время ждет окончания другого потока;
TERMINATED - поток завершен.

Пример использования Thread

В примере ChickenEgg рассматривается параллельная работа двух потоков (главный поток и поток Egg), в которых идет спор, «что было раньше, яйцо или курица?». Каждый поток высказывает свое мнение после небольшой задержки, формируемой методом ChickenEgg.getTimeSleep(). Побеждает тот поток, который последним говорит свое слово.

Package example; import java.util.Random; class Egg extends Thread { @Override public void run() { for(int i = 0; i < 5; i++) { try { // Приостанавливаем поток sleep(ChickenEgg.getTimeSleep()); System.out.println("Яйцо"); }catch(InterruptedException e){} } } } public class ChickenEgg { public static int getTimeSleep() { final Random random = new Random(); int tm = random.nextInt(1000); if (tm < 10) tm *= 100; else if (tm < 100) tm *= 10; return tm; } public static void main(String args) { Egg egg = new Egg (); // Создание потока System.out.println("Начинаем спор: кто появился первым?"); egg.start(); // Запуск потока for(int i = 0; i < 5; i++) { try { // Приостанавливаем поток Thread.sleep(ChickenEgg.getTimeSleep()); System.out.println("Курица"); }catch(InterruptedException e){} } if(egg.isAlive()) { // Cказало ли яйцо последнее слово? try { // Ждем, пока яйцо закончит высказываться egg.join(); } catch (InterruptedException e){} System.out.println("Первым появилось яйцо!!!"); } else { //если оппонент уже закончил высказываться System.out.println("Первой появилась курица!!!"); } System.out.println("Спор закончен"); } }

Начинаем спор: кто появился первым? Курица Курица Яйцо Курица Яйцо Яйцо Курица Курица Яйцо Яйцо Первым появилось яйцо!!! Спор закончен

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

Интерфейс Runnable

Интерфейс Runnable содержит только один метод run() :

Interface Runnable { void run(); }

Метод run() выполняется при запуске потока. После определения объекта Runnable он передается в один из конструкторов класса Thread .

Пример класса RunnableExample, реализующего интерфейс Runnable

Package example; class MyThread implements Runnable { Thread thread; MyThread() { thread = new Thread(this, "Дополнительный поток"); System.out.println("Создан дополнительный поток " + thread); thread.start(); } @Override public void run() { try { for (int i = 5; i > 0; i--) { System.out.println("\tдополнительный поток: " + i); Thread.sleep(500); } } catch (InterruptedException e) { System.out.println("\tдополнительный поток прерван"); } System.out.println("\tдополнительный поток завершён"); } } public class RunnableExample { public static void main(String args) { new MyThread(); try { for (int i = 5; i > 0; i--) { System.out.println("Главный поток: " + i); Thread.sleep(1000); } } catch (InterruptedException e) { System.out.println("Главный поток прерван"); } System.out.println("Главный поток завершён"); } }

При выполнении программы в консоль было выведено следующее сообщение.

Создан дополнительный поток Thread[Дополнительный поток,5,main] Главный поток: 5 дополнительный поток: 5 дополнительный поток: 4 Главный поток: 4 дополнительный поток: 3 дополнительный поток: 2 Главный поток: 3 дополнительный поток: 1 дополнительный поток завершён Главный поток: 2 Главный поток: 1 Главный поток завершён

Синхронизация потоков, synchronized

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

Package example; class CommonObject { int counter = 0; } class CounterThread implements Runnable { CommonObject res; CounterThread(CommonObject res) { this.res = res; } @Override public void run() { // synchronized(res) { res.counter = 1; for (int i = 1; i < 5; i++){ System.out.printf(""%s" - %d\n", Thread.currentThread().getName(), res.counter); res.counter++; try { Thread.sleep(100); } catch(InterruptedException e){} } // } } } public class SynchronizedThread { public static void main(String args) { CommonObject commonObject= new CommonObject(); for (int i = 1; i < 6; i++) { Thread t; t = new Thread(new CounterThread(commonObject)); t.setName("Поток " + i); t.start(); } } }

В примере определен общий ресурс в виде класса CommonObject, в котором имеется целочисленное поле counter. Данный ресурс используется внутренним классом , создающим поток CounterThread для увеличения в цикле значения counter на единицу. При старте потока полю counter присваивается значение 1. После завершения работы потока значение res.counter должно быть равно 4.

Две строчки кода класса CounterThread закомментированы. О них речь пойдет ниже.

В главном классе программы SynchronizedThread.main запускается пять потоков. То есть, каждый поток должен в цикле увеличить значение res.counter с единицы до четырех; и так пять раз. Но результат работы программы, отображаемый в консоли, будет иным:

"Поток 4" - 1 "Поток 2" - 1 "Поток 1" - 1 "Поток 5" - 1 "Поток 3" - 1 "Поток 2" - 6 "Поток 4" - 7 "Поток 3" - 8 "Поток 5" - 9 "Поток 1" - 10 "Поток 2" - 11 "Поток 4" - 12 "Поток 5" - 13 "Поток 3" - 13 "Поток 1" - 15 "Поток 4" - 16 "Поток 2" - 16 "Поток 3" - 18 "Поток 5" - 18 "Поток 1" - 20

То есть, с общим ресурсов res.counter работают все потоки одновременно, поочередно изменяя значение.

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

Блокировка на уровне объекта

Блокировать общий ресурс можно на уровне объекта, но нельзя использовать для этих целей примитивные типы. В примере следует удалить строчные комментарии в классе CounterThread, после чего общий ресурс будет блокироваться как только его захватит один из потоков; остальные потоки будут ждать в очереди освобождения ресурса. Результат работы программы при синхронизации доступа к общему ресурсу резко изменится:

"Поток 1" - 1 "Поток 1" - 2 "Поток 1" - 3 "Поток 1" - 4 "Поток 5" - 1 "Поток 5" - 2 "Поток 5" - 3 "Поток 5" - 4 "Поток 4" - 1 "Поток 4" - 2 "Поток 4" - 3 "Поток 4" - 4 "Поток 3" - 1 "Поток 3" - 2 "Поток 3" - 3 "Поток 3" - 4 "Поток 2" - 1 "Поток 2" - 2 "Поток 2" - 3 "Поток 2" - 4

Следующий код демонстрирует порядок использования оператора synchronized для блокирования доступа к объекту.

Synchronized (оbject) { // other thread safe code }

Блокировка на уровне метода и класса

Блокировать доступ к ресурсам можно на уровне метода и класса. Следующий код показывает, что если во время выполнения программы имеется несколько экземпляров класса DemoClass, то только один поток может выполнить метод demoMethod(), для других потоков доступ к методу будет заблокирован. Это необходимо когда требуется сделать определенные ресурсы потокобезопасными.

Public class DemoClass { public synchronized static void demoMethod(){ // ... } } // или public class DemoClass { public void demoMethod(){ synchronized (DemoClass.class) { // ... } } }

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

Некоторые важные замечания использования synchronized

  1. Синхронизация в Java гарантирует, что два потока не могут выполнить синхронизированный метод одновременно.
  2. Оператор synchronized можно использовать только с методами и блоками кода, которые могут быть как статическими, так и не статическими.
  3. Если один из потоков начинает выполнять синхронизированный метод или блок, то этот метод/блок блокируются. Когда поток выходит из синхронизированного метода или блока JVM снимает блокировку. Блокировка снимается, даже если поток покидает синхронизированный метод после завершения из-за каких-либо ошибок или исключений.
  4. Синхронизация в Java вызывает исключение NullPointerException, если объект, используемый в синхронизированном блоке, не определен, т.е. равен null.
  5. Синхронизированные методы в Java вносят дополнительные затраты на производительность приложения. Поэтому следует использовать синхронизацию, когда она абсолютно необходима.
  6. В соответствии со спецификацией языка нельзя использовать synchronized в конструкторе, т.к. приведет к ошибке компиляции.

Примечание: для синхронизации потоков можно использовать объекты синхронизации Synchroniser"s пакета java.util.concurrent .

Взаимная блокировка

С использованием блокировок необходимо быть очень внимательным, чтобы не создать «взаимоблокировку», которая хорошо известна разработчикам. Этот термин означает, что один из потоков ждет от другого освобождения заблокированного им ресурса, в то время как сам также заблокировал один из ресурсов, доступа к которому ждёт второй поток. В данном процессе могут участвовать два и более потоков.

Основные условия возникновения взаимоблокировок в многопотоковом приложении:

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

Взаимодействие между потоками в Java, wait и notify

При взаимодействии потоков часто возникает необходимость приостановки одних потоков и их последующего извещения о завершении определенных действий в других потоков. Так например, действия первого потока зависят от результата действий второго потока, и надо каким-то образом известить первый поток, что второй поток произвел/завершил определенную работу. Для подобных ситуаций используются методы:

  • wait() - освобождает монитор и переводит вызывающий поток в состояние ожидания до тех пор, пока другой поток не вызовет метод notify();
  • notify() - продолжает работу потока, у которого ранее был вызван метод wait();
  • notifyAll() - возобновляет работу всех потоков, у которых ранее был вызван метод wait().

Все эти методы вызываются только из синхронизированного контекста (синхронизированного блока или метода).

Рассмотрим пример «Производитель-Склад-Потребитель» (Producer-Store-Consumer). Пока производитель не поставит на склад продукт, потребитель не может его забрать. Допустим производитель должен поставить 5 единиц определенного товара. Соответственно потребитель должен весь товар получить. Но, при этом, одновременно на складе может находиться не более 3 единиц товара. При реализации данного примера используем методы wait() и notify() .

Листинг класса Store

Package example; public class Store { private int counter = 0; public synchronized void get() { while (counter < 1) { try { wait(); } catch (InterruptedException e) {} } counter--; System.out.println("-1: товар забрали"); System.out.println("\tколичество товара на складе: " + counter); notify(); } public synchronized void put() { while (counter >= 3) { try { wait(); }catch (InterruptedException e) {} } counter++; System.out.println("+1: товар добавили"); System.out.println("\tколичество товара на складе: " + counter); notify(); } }

Класс Store содержит два синхронизированных метода для получения товара get() и для добавления товара put() . При получении товара выполняется проверка счетчика counter. Если на складе товара нет, то есть counter < 1, то вызывается метод wait() , который освобождает монитор объекта Store и блокирует выполнение метода get() , пока для этого монитора не будет вызван метод notify() .

При добавлении товара также выполняется проверка количества товара на складе. Если на складе больше 3 единиц товара, то поставка товара приостанавливается и вызывается метод notify() , который передает управление методу get() для завершения цикла while().

Листинги классов Producer и Consumer

Классы Producer и Consumer реализуют интерфейс Runnable , методы run() у них переопределены. Конструкторы этих классов в качестве параметра получают объект склад Store. При старте данных объектов в виде отдельных потоков в цикле вызываются методы put() и get() класса Store для «добавления» и «получения» товара.

Package example; public class Producer implements Runnable { Store store; Producer(Store store) { this.store=store; } @Override public void run() { for (int i = 1; i < 6; i++) { store.put(); } } } public class Consumer implements Runnable { Store store; Consumer(Store store) { this.store=store; } @Override public void run(){ for (int i = 1; i < 6; i++) { store.get(); } } }

Листинг класса Trade

В главном потоке класса Trade (в методе main ) создаются объекты Producer-Store-Consumer и стартуются потоки производителя и потребителя.

Package example; public class Trade { public static void main(String args) { Store store = new Store(); Producer producer = new Producer(store); Consumer consumer = new Consumer(store); new Thread(producer).start(); new Thread(consumer).start(); } }

При выполнении программы в консоль будут выведены следующие сообщения:

1: товар добавили количество товара на складе: 1 +1: товар добавили количество товара на складе: 2 +1: товар добавили количество товара на складе: 3 -1: товар забрали количество товара на складе: 2 -1: товар забрали количество товара на складе: 1 -1: товар забрали количество товара на складе: 0 +1: товар добавили количество товара на складе: 1 +1: товар добавили количество товара на складе: 2 -1: товар забрали количество товара на складе: 1 -1: товар забрали количество товара на складе: 0

Поток-демон, daemon

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

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

Объявить поток демоном достаточно просто. Для этого нужно перед запуском потока вызвать его метод setDaemon(true). Проверить, является ли поток daemon "ом можно вызовом метода isDaemon(). В качестве примера использования daemon-потока можно рассмотреть класс Trade, который принял бы следующий вид:

Package example; public class Trade { public static void main(String args) { Producer producer = new Producer(store); Consumer consumer = new Consumer(store); // new Thread(producer).start(); // new Thread(consumer).start(); Thread tp = new Thread(producer); Thread tc = new Thread(consumer); tp.setDaemon(true); tc.setDaemon(true); tp.start(); tc.start(); try { Thread.sleep(100); } catch (InterruptedException e) {} System.out.println("\nГлавный поток завершен\n"); System.exit(0); } }

Здесь можно самостоятельно поэкспериментировать с определением daemon-потока для одного из классов (producer, consumer) или обоих классов, и посмотреть, как система (JVM) будет вести себя.

Thread и Runnable, что выбрать?

Зачем нужно два вида реализации многопоточности; какую из них и когда использовать? Ответ несложен. Реализация интерфейса Runnable используется в случаях, когда класс уже наследует какой-либо родительский класс и не позволяет расширить класс Thread . К тому же хорошим тоном программирования в java считается реализация интерфейсов. Это связано с тем, что в java может наследоваться только один родительский класс. Таким образом, унаследовав класс Thread , невозможно наследовать какой-либо другой класс.

Расширение класса Thread целесообразно использовать в случае необходимости переопределения других методов класса помимо метода run().

Приоритеты выполнения и голодание

Иногда разработчики используют приоритеты выполнения потока. В Java есть планировщик потоков (Thread Scheduler ), который контролирует все запущенные потоки и решает, какие потоки должны быть запущены и какая строка кода должна выполняться. Решение основывается на приоритете потока. Поэтому потоки с меньшим приоритетом получают меньше процессорного времени по сравнению с потоками с бо́льшим приоритет. Данное разумное решением может стать причиной проблем при злоупотреблении. То есть, если бо́льшую часть времени исполняются потоки с высоким приоритетом, то низкоприоритетные потоки начинают «голодать», поскольку не получают достаточно времени для того, чтобы выполнить свою работу должным образом. Поэтому рекомендуется задавать приоритет потока только тогда, когда для этого имеются веские основания.

Неочевидный пример «голодания» потока даёт метод finalize() , предоставляющий возможность выполнить код перед тем, как объект будет удалён сборщиком мусора. Однако приоритет финализирующего потока невысокий. Следовательно, возникают предпосылки для потокового голодания, когда методы finalize() объекта тратят слишком много времени (большие задержки) по сравнению с остальным кодом.

Другая проблема со временем исполнения может возникнуть от того, что не был определен порядок прохождения потоком блока synchronized . Когда несколько параллельных потоков должны выполнить некоторый код, оформленный блоком synchronized , может получиться так, что одним потокам придётся ждать дольше других, прежде чем войти в блок. Теоретически они могут вообще туда не попасть.

Скачать примеры

Рассмотренные на странице примеры многопоточности и синхронизации потоков в виде проекта Eclipse можно скачать (14Кб).

Any application can have multiple processes (instances). Each of this process can be assigned either as a single thread or multiple threads.

We will see in this tutorial how to perform multiple tasks at the same time and also learn more about threads and synchronization between threads.

In this tutorial, we will learn-

What is Single Thread?

A single thread is basically a lightweight and the smallest unit of processing. Java uses threads by using a "Thread Class".

There are two types of thread – user thread and daemon thread (daemon threads are used when we want to clean the application and are used in the background).

When an application first begins, user thread is created. Post that, we can create many user threads and daemon threads.

Single Thread Example:

Package demotest; public class GuruThread { public static void main(String args) { System.out.println("Single Thread"); } }

Advantages of single thread:

  • Reduces overhead in the application as single thread execute in the system
  • Also, it reduces the maintenance cost of the application.

What is Multithreading?

Multithreading in java is a process of executing two or more threads simultaneously to maximum utilization of CPU.

Multithreaded applications are where two or more threads run concurrently; hence it is also known as Concurrency in Java. This multitasking is done, when multiple processes share common resources like CPU, memory, etc.

Each thread runs parallel to each other. Threads don"t allocate separate memory area; hence it saves memory. Also, context switching between threads takes less time.

Example of Multi thread:

Package demotest; public class GuruThread1 implements Runnable { public static void main(String args) { Thread guruThread1 = new Thread("Guru1"); Thread guruThread2 = new Thread("Guru2"); guruThread1.start(); guruThread2.start(); System.out.println("Thread names are following:"); System.out.println(guruThread1.getName()); System.out.println(guruThread2.getName()); } @Override public void run() { } }

Advantages of multithread:

  • The users are not blocked because threads are independent, and we can perform multiple operations at times
  • As such the threads are independent, the other threads won"t get affected if one thread meets an exception.

Thread Life Cycle in Java

The Lifecycle of a thread:

There are various stages of life cycle of thread as shown in above diagram:

  1. Runnable
  2. Running
  3. Waiting
  1. New: In this phase, the thread is created using class "Thread class".It remains in this state till the program starts the thread. It is also known as born thread.
  2. Runnable: In this page, the instance of the thread is invoked with a start method. The thread control is given to scheduler to finish the execution. It depends on the scheduler, whether to run the thread.
  3. Running: When the thread starts executing, then the state is changed to "running" state. The scheduler selects one thread from the thread pool, and it starts executing in the application.
  4. Waiting: This is the state when a thread has to wait. As there multiple threads are running in the application, there is a need for synchronization between threads. Hence, one thread has to wait, till the other thread gets executed. Therefore, this state is referred as waiting state.
  5. Dead: This is the state when the thread is terminated. The thread is in running state and as soon as it completed processing it is in "dead state".

Some of the commonly used methods for threads are:

Method Description
start() This method starts the execution of the thread and JVM calls the run() method on the thread.
Sleep(int milliseconds) This method makes the thread sleep hence the thread"s execution will pause for milliseconds provided and after that, again the thread starts executing. This help in synchronization of the threads.
getName() It returns the name of the thread.
setPriority(int newpriority) It changes the priority of the thread.
yield () It causes current thread on halt and other threads to execute.

Example: In this example we are going to create a thread and explore built-in methods available for threads.

Package demotest; public class thread_example1 implements Runnable { @Override public void run() { } public static void main(String args) { Thread guruthread1 = new Thread(); guruthread1.start(); try { guruthread1.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } guruthread1.setPriority(1); int gurupriority = guruthread1.getPriority(); System.out.println(gurupriority); System.out.println("Thread Running"); } }

Explanation of the code:

Code Line 2: We are creating a class "thread_Example1" which is implementing the Runnable interface (it should be implemented by any class whose instances are intended to be executed by the thread.)

Code Line 4: It overrides run method of the runnable interface as it is mandatory to override that method

Code Line 6: Here we have defined the main method in which we will start the execution of the thread.

Code Line 7: Here we are creating a new thread name as "guruthread1" by instantiating a new class of thread.

Code Line 8: we will use "start" method of the thread using "guruthread1" instance. Here the thread will start executing.

Code Line 10: Here we are using the "sleep" method of the thread using "guruthread1" instance. Hence, the thread will sleep for 1000 milliseconds.

Code 9-14: Here we have put sleep method in try catch block as there is checked exception which occurs i.e. Interrupted exception.

Code Line 15: Here we are setting the priority of the thread to 1 from whichever priority it was

Code Line 16: Here we are getting the priority of the thread using getPriority()

Code Line 17: Here we are printing the value fetched from getPriority

Code Line 18: Here we are writing a text that thread is running.

5 is the Thread priority, and Thread Running is the text which is the output of our code.

Java Thread Synchronization

In multithreading, there is the asynchronous behavior of the programs. If one thread is writing some data and another thread which is reading data at the same time, might create inconsistency in the application.

When there is a need to access the shared resources by two or more threads, then synchronization approach is utilized.

Java has provided synchronized methods to implement synchronized behavior.

In this approach, once the thread reaches inside the synchronized block, then no other thread can call that method on the same object. All threads have to wait till that thread finishes the synchronized block and comes out of that.

In this way, the synchronization helps in a multithreaded application. One thread has to wait till other thread finishes its execution only then the other threads are allowed for execution.

It can be written in the following form:

Synchronized(object) { //Block of statements to be synchronized }

Java Multithreading Example

In this example, we will take two threads and fetch the names of the thread.

Example1:

GuruThread1.java package demotest; public class GuruThread1 implements Runnable{ /** * @param args */ public static void main(String args) { Thread guruThread1 = new Thread("Guru1"); Thread guruThread2 = new Thread("Guru2"); guruThread1.start(); guruThread2.start(); System.out.println("Thread names are following:"); System.out.println(guruThread1.getName()); System.out.println(guruThread2.getName()); } @Override public void run() { } }

Explanation of the code:

Code Line 3: We have taken a class "GuruThread1" which implements Runnable (it should be implemented by any class whose instances are intended to be executed by the thread.)

Code Line 8: This is the main method of the class

Code Line 9: Here we are instantiating the Thread class and creating an instance named as "guruThread1" and creating a thread.

Code Line 10: Here we are instantiating the Thread class and creating an instance named a "guruThread2" and creating a thread.

Code Line 11: We are starting the thread i.e. guruThread1.

Code Line 12: We are starting the thread i.e. guruThread2.

Code Line 13: Outputting the text as "Thread names are following:"

Code Line 14: Getting the name of thread 1 using method getName() of the thread class.

Code Line 15: Getting the name of thread 2 using method getName() of the thread class.

When you execute the above code, you get the following output:

Thread names are being outputted here as

  • Guru1
  • Guru2

Example 2:

In this example, we will learn about overriding methods run() and start() method of a runnable interface and create two threads of that class and run them accordingly.

Also, we are taking two classes,

  • One which will implement the runnable interface and
  • Another one which will have the main method and execute accordingly.
package demotest; public class GuruThread2 { public static void main(String args) { // TODO Auto-generated method stub GuruThread3 threadguru1 = new GuruThread3("guru1"); threadguru1.start(); GuruThread3 threadguru2 = new GuruThread3("guru2"); threadguru2.start(); } } class GuruThread3 implements Runnable { Thread guruthread; private String guruname; GuruThread3(String name) { guruname = name; } @Override public void run() { System.out.println("Thread running" + guruname); for (int i = 0; i < 4; i++) { System.out.println(i); System.out.println(guruname); try { Thread.sleep(1000); } catch (InterruptedException e) { System.out.println("Thread has been interrupted"); } } } public void start() { System.out.println("Thread started"); if (guruthread == null) { guruthread = new Thread(this, guruname); guruthread.start(); } } }

Explanation of the code:

Code Line 2: Here we are taking a class "GuruThread2" which will have the main method in it.

Code Line 4: Here we are taking a main method of the class.

Code Line 6-7: Here we are creating an instance of class GuruThread3 (which is created in below lines of the code) as "threadguru1" and we are starting the thread.

Code Line 8-9: Here we are creating another instance of class GuruThread3 (which is created in below lines of the code) as "threadguru2" and we are starting the thread.

Code Line 11: Here we are creating a class "GuruThread3" which is implementing the runnable interface (it should be implemented by any class whose instances are intended to be executed by the thread.)

Code Line 13-14: we are taking two class variables from which one is of the type thread class and other of the string class.

Code Line 15-18: we are overriding the GuruThread3 constructor, which takes one argument as string type (which is threads name) that gets assigned to class variable guruname and hence the name of the thread is stored.

Code Line 20: Here we are overriding the run() method of the runnable interface.

Code Line 21: We are outputting the thread name using println statement.

Code Line 22-31: Here we are using a for loop with counter initialized to 0, and it should not be less than 4 (we can take any number hence here loop will run 4 times) and incrementing the counter. We are printing the thread name and also making the thread sleep for 1000 milliseconds within a try-catch block as sleep method raised checked exception.

Code Line 33: Here we are overriding start method of the runnable interface.

Code Line 35: We are outputting the text "Thread started".

Code Line 36-40: Here we are taking an if condition to check whether class variable guruthread has value in it or no. If its null then we are creating an instance using thread class which takes the name as a parameter (value for which was assigned in the constructor). After which the thread is started using start() method.

When you execute the above code you get the following output:

There are two threads hence, we get two times message "Thread started".

We get the names of the thread as we have outputted them.

It goes into for loop where we are printing the counter and thread name and counter starts with 0.

The loop executes three times and in between the thread is slept for 1000 milliseconds.

Hence, first, we get guru1 then guru2 then again guru2 because the thread sleeps here for 1000 milliseconds and then next guru1 and again guru1, thread sleeps for 1000 milliseconds, so we get guru2 and then guru1.

Summary :

In this tutorial, we saw multithreaded applications in Java and how to use single and multi threads.

  • In multithreading, users are not blocked as threads are independent and can perform multiple operations at time
  • Various stages of life cycle of the thread are,
    • Runnable
    • Running
    • Waiting
  • We also learned about synchronization between threads, which help the application to run smoothly.
  • Multithreading makes many more application tasks easier.


Понравилась статья? Поделиться с друзьями: