Главная

Популярная публикация

Научная публикация

Случайная публикация

Обратная связь

ТОР 5 статей:

Методические подходы к анализу финансового состояния предприятия

Проблема периодизации русской литературы ХХ века. Краткая характеристика второй половины ХХ века

Ценовые и неценовые факторы

Характеристика шлифовальных кругов и ее маркировка

Служебные части речи. Предлог. Союз. Частицы

КАТЕГОРИИ:






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




 

При выполнении программы объект класса Thread может быть в одном из

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

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

 

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

NEW – поток создан, но еще не запущен;

RUNNABLE – поток выполняется;

BLOCKED – поток блокирован;

WAITING – поток ждет окончания работы другого потока;

TIMED_WAITING – поток некоторое время ждет окончания другого потока;

TERMINATED — поток завершен.

Получить значение состояния потока можно вызовом метода getState().

Поток переходит в состояние «неработоспособный» (WAITING) вызовом методов wait(), suspend() (deprecated-метод) или методов ввода/вывода, которые предполагают задержку.

Для задержки потока на некоторое время (в миллисекундах) можно перевести его в режим ожидания (TIMED_WAITING) с помощью методов sleep(long millis) и wait(long timeout), при выполнении которого может генерироваться прерывание InterruptedException.

Вернуть потоку работоспособность после вызова метода suspend() можно методом resume() (deprecated-метод), а после вызова метода wait() – методами notify() или notifyAll().

Поток переходит в «пассивное» состояние (TERMINATED), если вызваны методы interrupt(), stop() (deprecated-метод) или метод run() завершил выполнение. После этого, чтобы запустить поток еще раз, необходимо создать новый объект потока.

Методы sleep() и yield()

 

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

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

 

public class YieldRunner {

public static void main(String[] args) {

new Thread() {

public void run() {

System.out.println("старт потока 1");

Thread.yield();

System.out.println("завершение 1");

}

}.start();

new Thread() {

public void run() {

System.out.println("старт потока 2");

System.out.println("завершение 2");

}

}.start();

}}

В результате может быть выведено:

старт потока 1

старт потока 2

завершение 2

завершение 1

Активизация метода yield() в коде метода run() первого объекта потока приведет к тому, что, скорее всего, первый поток будет остановлен на некоторый квант времени, что даст возможность другому потоку запуститься и выполнить свой код.

Метод join()

Метод join() блокирует работу потока, в котором он вызван, до тех пор, пока не будет закончено выполнение вызывающего метод потока.

Рассмотрим пример:

 

public class Example {

public static void main(String[] args)

throws InterruptedException{

Thread thread1 = new Thread (new ExampleRunnable(" B", 80));

Thread thread2 = new Thread (new ExampleRunnable("A", 130));

thread1.start();

thread1.join();

thread2.start();

}}

 

В примере сначала будут выведены 10 букв "B", а только потом 10 букв "А". Давайте разберемся почему так? В самом начале работы метода main() он запустит новый поток thread1, а после вызова метода thread1.join() привяжется к нему и будет ожидать его завершения. Как только поток thread1 завершит свою работу метод main() начнет выполняться дальше, а именно, запустит поток thread2 на выполнение.

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

Рассмотрим еще один пример. Запустим два потока. Один очень быстрый печатает букву "В", второй – медленный печатает букву "А".

 

public class Example {

public static void main(String[] args)

throws InterruptedException{

Thread thread1 = new Thread (new ExampleRunnable(" B", 80));

Thread thread2 = new Thread (new ExampleRunnable("A", 400));

thread1.start();

thread2.start();

thread1.join();

System.out.println("Start wait!");

thread2.join();

System.out.println("End!!!");

}}

B

B

B

B

A

B

B

B

B

B

A

B

Start wait!

A

A

A

A

A

A

A

A

End!!!

 

Поток метода main() привязывается к потоку thread1 и ждет его завершения. После чего, печатает Start wait! и привязывается к медленно выполняющемуся потоку thread2, ожидает его завершения и печатает End!!!

Как вы можете видеть метод join()вызывается для экземпляра класса Thread. А можно ли остановить другие потоки и дожидаться завершения потока метода main(), ведь у нас нет экземпляра данного потока (он стартуется автоматически JVM)? Оказывается, чтобы получить ссылку на тот поток, в котором мы находимся, следует вызвать статический метод currentThread() класса Thread.

Следующий код приведет к тому, что программа повиснет, так как метод main() будет ожидать завершения (смерти) самого себя. Такое явление в многопоточном программировании называется взаимная блокировка или dead lock.

 

public static void main(String[] args)

throws InterruptedException{

Thread thread = Thread.currentThread();

thread.join();

}

 

Достаточно записи вида: Thread.currentThread().join() и программа висит)))

Такая же ситуация может возникнуть если один поток привязывается к другому и ожидает его смерти, в то время как этот второй поток привязался к первому и тоже ожидает его смерти. В итоге имеем dead lock. Два потока ждут завершения друг друга.

Кто не понял – объясняю популярно…

«Заходя в ванную, Анжела забыла взять с собой халат. Обычно она может выйти в комнату и в неодетом виде, но, пока она была в ванной, в гости зашёл Антон, которому Анжела должна отдать флешку, которая лежит у неё в сумочке. Сам Антон в сумочку лезть отказывается, и требует, чтобы флешку отдала ему Анжела. Без флешки он не уйдёт. Анжела не может выйти в комнату пока там Антон. Антон ждёт, пока ему отдадут флешку, Анжела ждёт ухода Антона, после которого она может выйти и отдать флешку.»

Теперь понятно? Продолжаем)))

Dead lock можно разрешить путем использования условного метода join(long millis) с параметром, равным времени в миллисекундах, на которое нужно привязаться к потоку и ожидать его смерти. Если бы в примере было записано thread.join(1000), то через 1000 мс. поток метода main() отпустило бы и он продолжил свое выполнение. Напишем класс, в котором поток метода main()и порожденный им второй поток создадут dead lock.

public class Example {

public static void main(String[] args)

throws InterruptedException{

final Thread mainThread = Thread.currentThread();

Thread runThread = new Thread (new Runnable(){

public void run() {

try{

System.out.println("Run: wait for main! ");

mainThread.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

});

runThread.start();

System.out.println("Main: wait for run! ");

runThread.join();

}} Main: wait for run!Run: wait for main!Два потока будут привязаны к друг другу и это приведет к дидлоку. В каком порядке будет выведен текст предсказать трудно. Одна из особенностей потоков – это недетерменизм. Когда стартуют два потока невозможно предсказать, кто завершится раньше, кто первый, а кто последний. Они выполняются абсолютно автономно. Поэтому предсказать заранее, чей метод join() выполнится первым практически невозможно. Это иногда приводит к существенным трудностям программирования потоков в Java.Обратите внимание на то, что в классе при создании второго потока runThread был использован анонимный inner класс, в котором реализован метод run(). Такая странная конструкция встречается в Java очень часто. Будем с ней сталкиваться при обработке событий. При запуске такой программы компилятор создаст анонимный класс, который будет имплементить интерфейс Runnable, и инстанс (instance – экземпляр) этого класса. Вы, конечно, помните, что нельзя создавать экземпляры абстрактных классов и интерфейсов, если у них не реализованы методы. В данном случае мы реализовываем единственный метод интерфейса Runnable – метод run().В стандартном jdk нет возможности стартовать мгновенно много (целый пучок) потоков. Потоки могут порождаться только последовательно. Это же относится и к методу join(). Нельзя привязаться и ждать завершения сразу нескольких потоков – только по очереди. Однако в Java версии 5 был добавлен пакет java.util.concurrent, содержащий более 50 методов, которые расширяют возможности класса Thread по работе с потоками. Кого интересует - изучайте документацию))) Синхронизация потоков Рассмотрим концепцию Monitor, основанную на применении ключевого слова synchronized и методов класса Object: wait(), notify() и notifyAll(). Модель синхронизации потоков – монитор – это механизм управления связью между потоками придуманный Хором (Hoare, C.A.R). Монитор можно представить как маленький блок, который содержит только один поток. Как только поток входит в монитор, все другие потоки должны ждать, пока данный не выйдет из монитора. Таким образом, монитор можно использовать для защиты совместно разделяемых ресурсов от управления несколькими потоками одновременно. Синхронизировать код можно двумя способами. Оба используют ключевое слово synchronized. Первый способ – записать слово synchronized в сигнатуре метода (в разделе модификаторов), второй – создать синхронизированный блок. Например, так synchronized (object) {// операторы для синхронизации} Объект не должен быть равен null.В синхронизированном методе или блоке можно использовать методы wait(), notify() и notifyAll(). Все три метода можно вызывать только внутри синхронизированного кода, в противном случае вылетает исключение IllegalMonitorStateException. Метод wait() сообщает вызывающему потоку, что нужно уступить монитор и переходить в режим ожидания (спать), пока некоторый другой поток не войдет в тот же монитор и не вызовет notify(). Метод notifyAll() пробуждает все потоки, которые вызвали wait() и спят. Метод wait() бросает проверяемое исключение InterruptedException. Рассмотрим на простых примерах правила написания синхронизированных блоков и методов. Синхронизация выполняется по объекту. В примере программа повиснет. Есть вариант метода wait(timeout) с параметром. В скобках указывается количество миллисекунд, которое объект должен находиться в ожидающем состоянии.

public static void main(String[] args)

throws InterruptedException{

Object obj = new Object();

synchronized (obj) {

obj.wait();

}}

 

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

 

public static void main(String[] args)

throws InterruptedException{

Object obj0 = new Object();

Object obj = obj0;

synchronized (obj) {

obj0.wait();

}}

 

Нижний пример работать не будет (вылетит исключение IllegalMonitorStateException), так как создаются два разных объекта. Метод wait() вызван не для того объекта, по которому синхронизирован блок.

 

public static void main(String[] args)

throws InterruptedException{

synchronized (new Object) {

new Object.wait();

}}

 

Нижний фрагмент кода корректный, так как вызов метода wait() происходит в блоке, синхронизированном по объекту obj0. В этой же месте можно вызывать и метод wait() для объекта obj1. В Java можно создавать любое количество вложенных синхронизированных блоков. При этом поток запоминает, в какое количество синхронизированных блоков он вошел.

 

public static void main(String[] args)

throws InterruptedException{

Object obj0 = new Object();

Object obj1 = new Object();

synchronized (obj0) {

synchronized (obj1) {

obj0.wait();

}

}

}

 

Следующий код абсолютно корректный. В синхронизированном нестатическом методе f() можно вызвать метод notify(). Помните, что статический метод не имеет ссылки this!

 

public class Example{

public static void main(String[] args)

throws InterruptedException{

new Example().f();

}

 

public synchronized void f() {

this.notify();

}

}

 

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

 

public class Example{

public static void main(String[] args)

throws InterruptedException{

new Example().f();

}

 

public void f() {

synchronized (this) {

this.notify();

}

}

}


 

Но как быть, если существует необходимость вызвать методы wait(), notify() или notifyAll() в статическом методе. Как известно у статического метода нет ссылки this. В статическом методе данные методы можно вызвать для объекта класса, как это показано ниже:

 

public class Example{

public static void main(String[] args)

throws InterruptedException{

f();

}

 

public static synchronized void f() {

Class clazz = Example.class;

clazz.notify();

}

}

 

Такая запись вполне корректна. Класс является ссылочным типом данных, т.е. объектом для которого может быть вызван метод notify().

Можно видоизменить приведенный выше код, используя синхронизированный блок:

 

public class Example{

public static void main(String[] args)

throws InterruptedException{

f();

}

 

public static void f() {

Class clazz = Example.class;

synchronized (clazz) {

clazz.notify();

}

}

}

Рассмотрим пример. Имеется класс BlockedMethodCaller, реализующий интерфейс Runnable. В конструктор данного класса передается ссылка на объект и целое число k. В методе run() для данной ссылки вызывается метод f(), в который и передается целое число k.

Отдельно создается класс BlockedSetExample, имеющий метод f(). Идея состоит в том, чтобы создавать много потоков (в примере их 5), каждый из которых может вызывать синхронизируемый метод f().

ПРИМЕР 3.

public class BlockedMethodCaller implements Runnable {

private final BlockedSetExample ref;

private final int k;

 

public BlockedMethodCaller (BlockedSetExample ref, int k) {

this.ref = ref;

this.k = k;

}

@Override

public void run() {

try {

ref.f(k);

} catch (InterruptedException e) {

e.printStackTrace();}

}}

 

public class BlockedSetExample {

public static void main (String[] args) throws InterruptedException {

BlockedSetExample ref = new BlockedSetExample();

for (int k = 0; k < 5; k++) {

new Thread(new BlockedMethodCaller(ref, k)).start();

}}

 

public synchronized void f(int x) throws InterruptedException {

System.out.println("+"+x);

Thread.sleep(1000);

System.out.println("-"+x);

}}

В программе создается 5 потоков, которые вызывают синхронизируемый метод f() (см.рис). В самом методе f() поток засыпает на 1 сек. Каждый поток имеет свой номер – целое число x.

При входе потока в метод f() на экран выводится его номер со знаком "+", после того как поток проснется, на экран выводится его номер, но уже со знаком "-". Результат работы программы приводится ниже:

+0

-0

+4

-4

+3

-3

+1

-1

+2

-2

 

Как видно из результата работы программы, потоки входят в метод f() по очереди. Пока в методе спит один поток, никакой другой поток туда зайти не может. Как только, находящийся в методе поток просыпается и покидает его, в метод заходит следующий поток. Таким образом, никакие два и более потоков не могут находиться и одновременно работать в синхронизированной по одной переменной секции. Это свойство называется взаимное исключение (mutual exclusion) или мьютекс.

В примере потоки синхронизируются по одной ссылке ref. В метод f() она передается неявно как ссылка this. Давайте попробуем создавать для каждого потока отдельную ссылку:

 

public class BlockedSetExample {

public static void main (String[] args) throws InterruptedException {

for (int k = 0; k < 5; k++) {

new Thread(new BlockedMethodCaller(new BlockedSetExample(), k)).start();

}}

 

public synchronized void f(int x) throws InterruptedException {

System.out.println("+"+x);

Thread.sleep(1000);

System.out.println("-"+x);

}}

В итоге мы не наблюдаем синхронизацию потоков, несмотря на то, что в заголовке метода f() записано слово synchronized.

+2

+0

+1

+3

+4

-0

-2

-4

-3

-1

Это связано с тем, что теперь мы создаем пять разных объектов и каждый поток вызывает синхронизированный метод f() у своего объекта (см.рис.). Так как у каждого объекта по одному потоку, то конкурировать некому. Потоки одновременно могут зайти каждый в свой метод, поспать 1 сек и проснуться. Поэтому на экране сначала появились все плюсы, а потом минусы. Помните, что синхронизация осуществляется по объектам!!!

Можете поэкспериментировать и убрать из заголовка метода f() слово synchronized. Результат будет следующий:

+0

+3

+2

+4

+1

-0

-4

-2

-3

-1

Поведение потоков изменилось. Обратите внимание, несмотря на то, что стартовали потоки последовательно, начиная с нулевого, добежали до метода f() они в произвольном порядке (и проснулись тоже). Это один из примеров непредсказуемого поведения потоков.

Рассмотрим еще один интересный пример:

ПРИМЕР 4.

public class WaitMethodCaller implements Runnable {

private final WaitSetExample ref;

private final int k;

 

public WaitMethodCaller (WaitSetExample ref, int k) {

this.ref = ref;

this.k = k;

}

@Override

public void run() {

try {

ref.f(k);

} catch (InterruptedException e) {

e.printStackTrace();}

}}

 

public class WaitSetExample {

public static void main (String[] args) throws InterruptedException {

WaitSetExample ref = new WaitSetExample();

for (int k = 0; k < 5; k++) {

new Thread(new BlockedMethodCaller(ref, k)).start();

}}

 

public synchronized void f(int x) throws InterruptedException {

System.out.println("+"+x);

this.wait();

System.out.println("-"+x);

}}

+0

+2

+3

+4

+1

В синхронизируемом методе f() потоки теперь не спять 1 сек., а переводятся в режим ожидания методом wait(). Когда поток в режиме ожидания – разбудить его могут только методы notify() и notifyAll(), которых в программе нет. Как вы можете видеть по результату выполнения программы, все пять потоков смогли зайти в синхронизируемый по одному объекту метод. И каждый из них отдельно повиснул. Как же можно объяснить такой странный результат, ведь выше уже говорилось о том, что одновременно не более одного потока могут находиться и работать в синхронизируемой секции? Правильно работать, но не вызвать метод wait()!!! Единственный способ освободить блокировку синхронизируемой секции – вызвать для потока метод wait(). В этом случае потоки переходят в отдельное множество называемое wait-set.

С любым объектом в Java, находящимся в куче, связано три незримых множества. Схематично это можно представить следующим образом:

- блокировка. Объект может быть либо блокирован, либо не блокирован (в случае, когда в synchronized по этому объекту кто-то вошел, и никто другой уже туда не может войти).

- множество wait-set. Потоки внутри этого множества пассивные, они спят и ждут, когда их разбудят.

- множество blocked-set. Потоки в этом множестве как бы активные, но не работают (не грузят CPU), им что-то мешает работать.

Смоделируем ситуацию с блокировкой в примере 3. Когда в синхронизируемый метод вошел первый поток (№0) и установил блокировку, никакой другой поток туда зайти уже не может, и они все перемещаются во множество blocked-set (рис.а). В этом множестве потоки активные, но их временно приостановила ОС, до тех пор, пока не освободится блокировка. Как только поток №0 завершит работу и освободит блокировку, ОС случайным образом запустит в synchronized метод следующий поток из множества blocked-set (рис b).

Схематично изобразим действия, происходящие в примере 4. Поток захватывает синхронизированный метод и самостоятельно вызывает метод wait() (рис.а). При этом блокировка метода снимается, а сам поток помещается JVM в wait-set. Затем следующий поток ставит блокировку и захватывает метод (рис. b). Процесс повторяется до тех пор, пока все потоки не окажутся в множестве wait-set.

 






Не нашли, что искали? Воспользуйтесь поиском:

vikidalka.ru - 2015-2024 год. Все права принадлежат их авторам! Нарушение авторских прав | Нарушение персональных данных