17 апреля 2010 года в Москве, в офисе Яндекса, ул. Льва Толстого, 16, состоится встреча JUG.RU на которой выступит Евгений Кирпичёв с докладом “Многопоточное программирование и Java: корректность, паттерны, оптимизация”.
Цель доклада — расширить кругозор слушателей в области методик разработки многопоточных программ:
- формальные рассуждения о корректности
- способствующие ей приёмы проектирования
- способы тестирования
- вопросы эффективности
- инструментарий
- интересные средства многопоточного программирования из других языков
Для регистрации пришлите свою фамилию и имя на русском языке на yasha@telamon.ru?subject=17_April_2010. Регистрация мягкая, достаточно просто послать письмо, подтверждения не высылаются. После официальной части планируется совместное посещение бара для неформального общения.
Цель этого поста – вспомнить все про потоки в Java. С помощью этой статьи также можно подготовиться к сдаче экзамена SCJP 6 так как в ней рассматриваются все вопросы, которые могут возникнуть на экзамене по теме “потоки в Java”. Поехали!
Определение потока в Java
В Java термин “поток” может обозначать две разные вещи:
- Экземпляр класса java.lang.Thread
- Поток выполнения
Экземпляр Thread – это обычный объект. Подобно другим объектам в Java, он имеет переменные и методы, а живет и умирает в куче. Поток выполнения (thread of execution) – это отдельный процесс (“легковесный” процесс), имеющий свой собственный стэк вызовов. В Java для каждого потока существует один стэк вызовов. Даже если вы явно не создаете потоков в вашей программе, они там все равно есть.
Метод main(), например, выполняется в потоке, который называется “главным” (main thread), и если вы посмотрите на стэк вызовов, то увидите, что метод main() стоит первым в стеке, т.е. находится в самом низу.
Как только вы создадите новый поток, создастся новый стэк, в который будут помещаться записи о вызовах методов, совершенных из этого нового потока.
JVM от Sun, начиная с версии 1.2 мапит Java-потоки на нативные потоки операционной системы один-к-одному, однако у JVM свой планировщик потоков, который не зависит от планировщика операционной системы под которой работает JVM. Одно следует знать точно: когда дело доходит до потоков, то здесь нельзя давать ни каких гарантий. Нельзя точно определить как будут выполняться параллельные потоки. Ответственность за это полностью лежит на планировщике JVM.
В Java есть два вида потоков: потоки-демоны (daemon threads) и пользовательские потоки (user threads). Здесь мы будем рассматривать в основном user threads. Разница между этими двумя типами потоков в том, что JVM завершает выполнение программы когда все пользовательские потоки завершат свое выполнение. Как только завершил свое выполнение последний пользовательский поток, JVM остановится не зависимо от того, в каком состоянии находятся потоки-демоны.
Создание потоков
Поточность в Java начинается с создания экземпляра java.lang.Thread. В этом классе мы найдем методы для управления потоками: для создания, запуска, и приостановки потоков. Следующие методы самые популярные:
- start()
- yield()
- sleep()
- run()
Весь экшн происходит в методе run(). В него помещается код, который требуется выполнить в отдельном потоке.
public void run() { // код для выполнения в отдельном потоке }
Из метода run() конечно же можно вызывать другие методы, а так как для отдельного потока создался новый стэк вызовов, то в этом новом стеке, первым методом будет метод run(). А где нам разместить метод run()? Определить и инстанциировать поток можно двумя способами:
- Расширить класс java.lang.Thread
- Реализовать интерфейс Runnable
Предпочтительным способом является реализация интерфейса Runnable. Расширить Thread – это просто, но этот способ не является хорошей практикой ООП. Почему? Потому что в Java нет множественного наследования и расширяя Thread, вы не можете сделать этот класс подклассом другого класса. Поэтому, лучше всего разработать класс, который реализует интерфейс Runnable.
Определение потока
Хоть в предыдущем разделе мы и выяснили что расширение Thread это не good OOP practice, но такой способ определения потока все же существует и заслуживает рассмотрения.
Расширение java.lang.Thread
Простейший путь определить поток – расширить Thread, переопределить метод run() и поместить в нем весь код, который нужно выполнить в отдельном потоке:
package ru.topcode.threadomaniac; public class MyThread extends Thread { @Override public void run() { System.out.println("Очень важная работа выполняется в MyThread"); } }
Вы можете свободно использовать перегрузку метода run():
package ru.topcode.threadomaniac; public class MyThread extends Thread { @Override public void run() { System.out.println("Очень важная работа выполняется в MyThread"); } public void run(String s) { System.out.println("Строка из метода run: " + s); } }
Однако, не забывайте, что перегруженные run() игнорируются классом Thread. Класс Thread ожидает запуска именно run() без аргументов. Если вы вызовете run(String s), то вызов метода попадет в тот же стек вызовов, в котором лежит вызывающий его метод, т.е. отдельный поток не создастся.
Реализация интерфейса java.lang.Runnable
Реализация интерфейса Runnable дает возможность расширить любой другой класс и в то же время определить поведение, которое будет выполняться в отдельном потоке.
package ru.topcode.threadomaniac; class MyRunnable implements Runnable { @Override public void run() { System.out.println("Очень важная работа выполняется в MyRunnable"); } }
Независимо от того, какой механизм вы выбрали, вы получили некоторый код, который может быть запущен. Теперь давайте посмотрим как запустить поток.
Инстанцирование потока
Каждый поток начинается с создания экземпляра Thread. Независимо от того, расширяли вы Thread или реализовывали Runnable, для выполнения работы, нужно иметь экземпляр Thread.
Если вы расширили класс Thread, то инстанцирование выполняется также как инстанцирование обычного объекта:
MyThread t = new MyThread();
Если вы реализовывали Runnable, вам все равно придется создать экземпляр класса Thread.
MyRunnable r = new MyRunnable(); Thread t = new Thread(r);
Объяснить это можно так: вместо того, чтобы комбинировать в одном классе и поток и выполняемую работу (код), мы разделяем логику на два класса – класс Thread для потоко-зависимого кода и реализация Runnable для описания работы, которая должна выполняться в отдельном потоке. Другими словами: Thread – это работник, а Runnable – это работа.
Один экземпляр Runnable можно передать нескольким объектам Thread:
MyRunnable r = new MyRunnable(); Thread foo = new Thread(r); Thread bar = new Thread(r); Thread bat = new Thread(r);
Это означает что несколько потоков будут делать одну и ту же работу. Значит эта работа будет выполнена несколько раз.
Класс Thread тоже реализует Runnable, а значит в конструктор Thread можно передать экземпляр Thread:
Thread t = new Thread(new MyThread());
Выглядит глупо, но сделать так можно.
Кроме конструктора по умолчанию и конструктора, принимающего экземпляр Thread, в классе Thread есть и другие перегруженные конструкторы. Вот список всех конструкторов Thread:
- Thread()
- Thread(String name)
- Thread(Runnable runnable)
- Thread(Runnable runnable, String name)
- Thread(ThreadGroup g, Runnable runnable)
- Thread(ThreadGroup g, Runnable runnable, String name)
- Thread(ThreadGroup g, String name)
Мы рассмотрим каждый конструктор немного позже.
Итак, вы сделали себе экземпляр Thread и он знает что нужно выполнять метод run(). Но почему ничего не происходит? На данный момент мы имеем всего лишь обычный Java-объект типа Thread. Это еще не поток исполнения. Для того чтобы создать новый поток исполнения и новый стэк вызовов, нам нужно запустить поток.
Когда поток инстанцирован, но не запущен, говорят что он находится в состоянии new (новый). На этом этапе поток еще не считается живым (alive). После вызова метода start() экземпляра потока, поток переходит в состояние alive (живой). То что поток живой не означает что метод run() уже начал выполняться! Поток считается мертвым (dead) после того, как метод run() закончит выполняться. Метод isAlive() – лучший способ определить запущен ли поток и не завершил ли он уже выполняться. Часто при отладке используется метод getState().
Запуск потока
Пришло время запустить поток. Это настолько просто, что врядли заслуживает отдельного раздела:
t.start();
Что происходит после старта потока? А происходит следующее:
- Стартует новый поток выполнения (с новым стэком вызовов).
- Поток переходит из состояния new (новый) в состояние работоспособный (runnable).
- Когда поток получает шанс выполниться, он вызывает метод run().
Запомните, что мы вызываем метод start() экземпляра класса Thread. Следующий пример продемонстрирует все что мы рассмотрели выше:
package ru.topcode.threadomaniac; public class Starter { public static void main(String[] args) { FooRunnable r = new FooRunnable(); Thread t = new Thread(r); t.start(); } } class FooRunnable implements Runnable { public void run() { for (int x = 1; x < 6; x++) { System.out.println("Runnable running"); } } }
Еще раз заметьте, что для старта потока, нужно вызывать метод start() экземпляра класса Thread. Если вы вызовете метод run() из вашего класса, который реализует интерфейс Runnable или даже если вы вызовете run из класса, расширяющего Thread, то ничего страшного не произойдет. Не возникнет ни каких исключительных ситуаций относящихся к потокам. Метод просто выполнится, но новый поток не создастся! Метод выполнится в том же потоке, из которого был запущен! Следующий код не запускает новый поток выполнения:
Thread t = new Thread(); t.run(); // Так можно, но новый поток не создастся.
Так что же произойдет если запустить несколько потоков? Давайте рассмотрим пример запуска нескольких потоков. В следующем примере инстанцируются новые именованные потоки (потоки, которым мы явно присвоили имена). Имена здесь мы присвоили потокам чтобы проследить какой поток выполняется в данный момент времени.
package ru.topcode.threadomaniac; public class Starter { public static void main(String[] args) { NameRunnable nr = new NameRunnable(); Thread t = new Thread(nr); t.setName("Поток1"); t.start(); } } class NameRunnable implements Runnable { public void run() { System.out.println("NameRunnable запущен"); System.out.println("Выполняется " + Thread.currentThread().getName()); } }
В результате получим следующий вывод в консоль:
NameRunnable запущен
Выполняется Поток1Здесь, чтобы получить имя выполняющегося потока, мы использовали метод getName() экземпляра класса Thread. Статический метод Thread.currentThread() возвращает ссылку на текущий выполняемый поток.
Даже если вы явно не указываете имя потока, оно у него все равно будет. Закомментируйте строку
t.setName("Поток1");
и запустите программу. Вы получите следующий вывод в консоль:
NameRunnable запущен Выполняется Thread-0
Так как мы получаем имя текущего потока с помощью статического метода Thread.currentThread(), то можно также получить имя главного потока – main.
Вот наглядное представление того, что происходит со стеками во время работы многопоточного приложения:
Картинка из книги SCJP Sun Certified Programmer for Java 6 Study Guide (Exam 310-065).
Запуск и выполнение нескольких потоков
Мы поиграли с двумя потоками (main и Thread-0). Давайте теперь запустим побольше потоков и посмотрим как они будут работать:
package ru.topcode.threadomaniac; public class Starter { public static void main(String[] args) { NameRunnable nr = new NameRunnable(); Thread one = new Thread(nr); Thread two = new Thread(nr); Thread three = new Thread(nr); one.setName("Первый"); two.setName("Второй"); three.setName("Третий"); one.start(); two.start(); three.start(); } } class NameRunnable implements Runnable { public void run() { for (int x = 1; x <= 3; x++) { System.out.println("Запущен " + Thread.currentThread().getName() + ", x равен " + x); } } }
Я получил следующий вывод в консоль:
Запущен Третий, x равен 1 Запущен Третий, x равен 2 Запущен Третий, x равен 3 Запущен Первый, x равен 1 Запущен Первый, x равен 2 Запущен Первый, x равен 3 Запущен Второй, x равен 1 Запущен Второй, x равен 2 Запущен Второй, x равен 3
В такой последовательности выполнялись потоки у меня в этот раз. Однако, такое же поведение не гарантируется при следующем запуске программы. Также не гарантируется что когда поток начал выполняться, он будет продолжать выполняться пока не закончится его метод run(). В моем случае произошло именно так: каждый цикл успел полностью выполниться без прерываний. Я запустил эту же программу еще раз и получил следующий вывод:
Запущен Первый, x равен 1 Запущен Третий, x равен 1 Запущен Третий, x равен 2 Запущен Третий, x равен 3 Запущен Первый, x равен 2 Запущен Первый, x равен 3 Запущен Второй, x равен 1 Запущен Второй, x равен 2 Запущен Второй, x равен 3
В каждом отдельном потоке, порядок выполнения предсказуем, т.е., например, наши циклы всегда будут инкрементировать x и выводить в консоль сообщение, но порядок выполнения потоков не предсказуем. Велика вероятность того, что такой маленький и простой цикл всегда будет успевать выполняться до переключения на другой поток, но если увеличить длину цикла хотя бы до 400, можно увидеть, что потоки прерываются.
Так же не гарантируется что если поток1 начал выполняться первым, то он и закончит выполняться первым. Он может закончить свою работу и самым последним. Мы не контролируем планировщик потоков, поэтому не можем предсказать порядок выполнения, а соответственно, и порядок завершения потоков. Однако, существует способ сказать потоку чтобы не запускался пока какой-нибудь другой поток не закончился. Это можно сделать с помощью метода join(), который мы рассмотрим немного позже.
Когда метод run() потока завершился, поток перестает быть потоком исполнения, стек вызовов удаляется, а поток считается мертвым (dead). С этого момента поток – это обычный объект Thread. А можем ли мы снова запустить run() этого объекта? Нет! Если поток был запущен, то он никогда не может быть запущен повторно! Если у вас есть ссылка на поток, и вы вызовете его метод start() повторно, то получите IllegalThreadStateException. Запустить поток можно только из состояния new (новый), а состояние new он имеет только перед первым запуском. Если попытаться запустить поток из состояния runnable или dead, то получим IllegalThreadStateException.
До сих пор мы узнали о трех состояниях потока: новый (new), runnable (работоспособный), и dead (мертвый).
В дополнение к методам setName() и getName(), для идентификации потока можно еще использовать getId(). getId() возвращает положительное, уникальное число типа long и это число однозначно идентифицирует поток на протяжении всей его жизни.
Планировщик потоков
Планировщик потоков является частью JVM (хотя некоторые JVM мапят Java-потоки на нативные потоки ОС) и решает какой поток будет работать в определенный момент.
Предположим, что на нашем компьютере установлен только один процессор. Тогда реально, в определенный момент времени может выполняться только один поток, а решает какой именно поток – планировщик.
Любой поток, имеющий состояние runnable (работоспособный), может быть выбран планировщиком для выполнения. Если поток не в состоянии runnable, то он не может быть выбран планировщиком. Когда поток переходит в состояние runnable, он помещается в runnable pool из которого планировщик выбирает какой поток выполнять.
Хоть мы и не контролируем планировщик потоков (т.е. явно нельзя сказать какой поток запустить), мы можем влиять на него с помощью нескольких методов:
- public static void sleep(long millis) throws InterruptedException
- public static void yield()
- public final void join() throws InterruptedException
- public final void setPriority(int newPriority)
Методы sleep() и join() имеют также перегруженные версии, которые здесь не показаны.
Приведенные выше методы находятся в классе java.lang.Thread, но в классе java.lang.Object для работы с потоками также есть несколько методов:
- public final void wait() throws InterruptedException
- public final void notify()
- public final void notifyAll()
Метод wait() имеет три перегруженные версии.
Состояния потоков и переходы между состояниями
Мы уже знаем три состояния потоков – new, runnable и dead, но это еще не все. Планировщик потоков может переводить потоки в состояние running (работающий) и “отбирать” у них это состояние, т.е., например, переводить их обратно в состояние runnable или в какое-нибудь другое. Ниже мы рассмотрим все состояния, в которых может находиться поток и переходы между этими состояниями.
Состояния потоков
Поток может находиться в одном из пяти состояний:
- Новый (new). После создания экземпляра потока, он находится в состоянии Новый до тех пор, пока не вызван метод start(). В этом состоянии поток не считается живым.
- Работоспособный (runnable). Поток переходит в состояние Работоспособный, когда вызывается метод start(). Поток может перейти в это состояние также из состояния Работающий или из состояния Блокирован. Когда поток находится в этом состоянии, он считается живым.
- Работающий (running). Поток переходит из состояния Работоспособный в состояние Работающий, когда Планировщик потоков выбирает его из runnable pool как работающий в данный момент.
-
Ожидающий (waiting)/Заблокированный (blocked)/Спящий(sleeping). Эти состояния характеризуют поток как не готовый к работе. Я объединил эти состояния т.к. все они имеют общую черту – поток еще жив (alive), но в настоящее время не может быть выполнен. Другими словами, поток уже не runnable, но он может вернуться в это состояние. Поток может быть заблокирован – это может означать, например, что он ждет освобождения каких-нибудь ресурсов, и разблокируется когда эти ресурсы освободятся. Поток может спать потому, что в его методе run() встретился метод sleep(). Просыпается он тогда, когда время его сна истекло (таймер). Поток может находиться в состоянии ожидания если в его методе run() встретится метод wait(). Вызов notify() или notifyAll() может перевести поток из состояния Ожидания в состояние Работоспособный.
Важно отметить, что один поток не может попросить другой поток заблокироваться. Если в потоке у вас есть ссылка на экземпляр другого потока, то можно сделать следующее:
t.sleep(); или t.yield()
Но эффекта от этого будет ноль, т.к. это статические методы класса Thread, а значит они ни коим образом не влияют на какой-то конкретный экземпляр. Вместо этого они влияют на текущий поток. Это хороший пример того, почему использование экземпляра для доступа к статическим методам является плохой практикой.
- Мёртвый (dead). Поток считается мёртвым, когда его метод run() полностью выполнен. Мёртвый поток не может перейти ни в какое другое состояние, даже если для него вызван метод start(). Не нужно быть ученым чтобы понять что мёртвый не может ожить
Весь этот раздел можно представить одной картинкой:
Продолжение читайте в статье “Потоки в Java. Часть 2“.






#1 by Artem on 12 Апрель 2010 - 12:48
Quote
Насколько я знаю в JVM начиная с версии 1.2 используются native thread. А зеленые потоки (green threads) использовались только в первых версиях
#2 by Дмитрий Леонтьев on 12 Апрель 2010 - 14:09
Quote
Здравствуйте, Artem. Спасибо за полезную информацию. Да, действительно, начиная с JVM 1.2 все Java-потоки мапятся на нативные потоки в ОС 1 : 1. Как-то даже не задумывался об этом. Поправлю в статье.
Pingback: Потоки в Java. Часть 2. | Java EE Dev
#3 by Victor on 14 Апрель 2010 - 13:34
Quote
“Это только треть всего поста, поэтому разбиваю его на несколько частей” – перечитайте и перепишите это предложение.
#4 by Дмитрий Леонтьев on 14 Апрель 2010 - 15:03
Quote
Здравствуйте, Victor. Перечитал и удалил )) Спасибо.
#5 by Anton on 17 Апрель 2010 - 23:15
Quote
Просто огромное спасибо. Полезно и что не мало важно интересно провел время!
#6 by Евгений on 8 Июнь 2010 - 16:46
Quote
очень качественный стиль письма: очень доходчиво, основные моменты проиллюстрированы, большое спасибо за материал, погнал читать вторую часть.
#7 by Александр on 19 Июнь 2010 - 0:38
Quote
Статья отличная. Бэкграунд вырвиглазный.
#8 by TAIMOS on 5 Сентябрь 2010 - 16:51
Quote
Большое спасибо, Дмитрий за Ваши интереснейшие и полезные статьи.
С удовольствием прочел, вник и поэкспериментировал на практике. Данную тему даже Эккель хуже объяснил. В крайнем случае для меня
Пошел читать вторую часть.
#9 by TAIMOS on 5 Сентябрь 2010 - 16:57
Quote
Единственное, что не понял – это картинку со стеками. не особо она наглядна для начинающего.
#10 by Дмитрий Леонтьев on 13 Сентябрь 2010 - 14:40
Quote
Здравствуйте, Anton, Евгений, Александр, TAIMOS!
Рад что статьи оказались полезными для вас. Постараюсь продолжать в том же духе (или лучше).
Александр, вы наверно работаете через Google Chrome? Действительно, бэкраунд был вырвиглазный, но только в этом браузере. Какая-то проблема с шаблоном была. Сейчас я его переустановил и вроде бы все стало нормально.
TAIMOS, спасибо за такие слова. С Эккелем меня пока еще ни кто не сравнивал
Это приятно 
Но если найду более понятное изображение, сразу заменю.
Картинку со стеками взял из книги Kathy Sierra, Bert Bates. SCJP Sun Certified Programmer for Java 6 Exam 310-065. Она только на первый взгляд кажется непонятной, но если присмотреться, то она более чем понятна
#11 by Alex654 on 25 Ноябрь 2010 - 5:45
Quote
Спасибо, отличная статья. Очень доходчиво)
#12 by Иван on 30 Ноябрь 2010 - 19:52
Quote
Впервые встретил настолько отточенное и законченное описание потоков в Java. У меня дома два тома Хорстмана.
Без преувеличения могу сказать, что Хорстман “отдыхает” в описании потоков!
Хватило одного раза прочтения – в голове все само собой разложилось по полочкам.
Огромное спасибо за статью!
Творческих успехов и крепкого здоровья!
#13 by partizano on 13 Декабрь 2010 - 18:40
Quote
Если я все правильно понял, то в следующем тексте ошибка:
Класс Thread тоже реализует Runnable, а значит в конструктор Thread можно передать экземпляр Thread:
Thread t = new Thread(new MyThread());
Должно быть:
Класс Thread тоже реализует Runnable, а значит в конструктор Thread можно передать экземпляр Runnable:
Thread t = new Thread(new MyRunnable());
#14 by Дмитрий Леонтьев on 5 Январь 2011 - 1:51
Quote
Здравствуйте, partizano! Оба этих варианта верны. Можно написать так:
В этом случае вы расширяете класс Thread и передаете экземпляр своего класса, расширяющего Thread в конструктор Thread.
#15 by Studentka on 9 Январь 2011 - 3:09
Quote
Спасибо большое) Статья очень помогла. Приятно было читать. Я совсем еще пока чайник в потоках, только начала их учить, поэтому понимаю, как нужна хорошая и понятная литература. Хотелось бы, чтобы вышла Ваша книга. Я с удовольствием ее бы купила)
#16 by c0nst on 11 Февраль 2011 - 1:10
Quote
Классно написано. И радует, что есть еще 2 части на єту тему.
Кстати, метод stop() не стоит оставлять за бортом. При интеграции со внешними системами часто єто единственный способ предотвратить опустошение thread-pool’а, например, когда поток вошел в натив.
#17 by Дмитрий Леонтьев on 21 Февраль 2011 - 22:41
Quote
Здравствуйте, c0nst! Спасибо за информацию о методе stop(). Приму на заметку.
#18 by Дмитрий Леонтьев on 21 Февраль 2011 - 22:50
Quote
Здравствуйте, Studentka! Рад что статья помогла Вам в изучении потоков. Книгу пока писать не собираюсь, сам еще зелёный
#19 by Артем on 7 Сентябрь 2011 - 18:03
Quote
Здравствуйте, Дмитрий) Очень хорошая статья, спасибо большое!
Хочу спросить по поводу перехода из одного состояния в другое, я вроде понял но хотелось бы уточнить.
Когда происходит метод start, поток запускается и стек добавляется метод run, и в данный момент поток находиться в состоянии Runnable, а переход в состояние Running происходит как только начал выполняться метод run.
И на сколько я понял, эти два состояния сделаны отдельно потому, что вызов метода start не гарантирует незамедлительный вызова метода run.
Скажите пожалуйста я правильно полнял ? или все же где то я не прав ?
#20 by Билал on 29 Сентябрь 2011 - 22:12
Quote
не плохо было бы добавить роль имен потоков, мне как новичку не понятно зачем давать имена потокам, и как это пригодится в дальнейшем
#21 by Avoitadoatoli on 19 Октябрь 2011 - 23:32
Quote
Медицинская литература – для студентов, преподавателей, медиков! Книги онлайн, бесплатно!
#22 by Igor on 21 Декабрь 2011 - 1:34
Quote
Реально классная статья, очень доходчиво, картинки – все как надо. Автору респект.
#23 by Anton on 13 Январь 2012 - 16:33
Quote
Статья оч гуд учу java было интересно почитать, доступно и по шагам 5++
интересный момент касательно примера….потоки никогда не менялись.. почему такое может быть??
package Stream;
public class TestStream{
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
MyRun y= new MyRun();
Thread t=new Thread(y);
Thread t2=new Thread(y);
Thread t3=new Thread(y);
t.setName(” Первый поток”);
t.start();
t2.setName(” Второй поток”);
t2.start();
t3.setName(” Третий поток”);
t3.start();
}
}
class MyRun implements Runnable{
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<5; i++){
System.out.println("Запущен"+Thread.currentThread().getName()+": квадрат i= "+i*i);
}
}
}
В консоли:
Запущен Первый поток: квадрат i= 0
Запущен Первый поток: квадрат i= 1
Запущен Первый поток: квадрат i= 4
Запущен Первый поток: квадрат i= 9
Запущен Первый поток: квадрат i= 16
Запущен Второй поток: квадрат i= 0
Запущен Второй поток: квадрат i= 1
Запущен Второй поток: квадрат i= 4
Запущен Второй поток: квадрат i= 9
Запущен Второй поток: квадрат i= 16
Запущен Третий поток: квадрат i= 0
Запущен Третий поток: квадрат i= 1
Запущен Третий поток: квадрат i= 4
Запущен Третий поток: квадрат i= 9
Запущен Третий поток: квадрат i= 16
#24 by Pavel on 25 Январь 2012 - 19:31
Quote
Спасибо! Все доходчиво и понятно, пошел читать дальше)
#25 by localhost on 27 Январь 2012 - 15:30
Quote
последний рисунок – в блок где: Waiting/blocking – еще sleep входит