Thread yield java что это
Собеседование по Java — многопоточность (вопросы и ответы)
Вопросы и ответы для собеседования Java по теме — многопоточность.
К списку вопросов по всем темам
Вопросы
1. Дайте определение понятию “процесс”.
2. Дайте определение понятию “поток”.
3. Дайте определение понятию “синхронизация потоков”.
4. Как взаимодействуют программы, процессы и потоки?
5. В каких случаях целесообразно создавать несколько потоков?
6. Что может произойти если два потока будут выполнять один и тот же код в программе?
7. Что вы знаете о главном потоке программы?
8. Какие есть способы создания и запуска потоков?
9. Какой метод запускает поток на выполнение?
10. Какой метод описывает действие потока во время выполнения?
11. Когда поток завершает свое выполнение?
12. Как синхронизировать метод?
13. Как принудительно остановить поток?
14. Дайте определение понятию “поток-демон”.
15. Как создать поток-демон?
16. Как получить текущий поток?
17. Дайте определение понятию “монитор”.
18. Как приостановить выполнение потока?
19. В каких состояниях может пребывать поток?
20. Что является монитором при вызове нестатического и статического метода?
21. Что является монитором при выполнении участка кода метода?
22. Какие методы позволяют синхронизировать выполнение потоков?
23. Какой метод переводит поток в режим ожидания?
24. Какова функциональность методов notify и notifyAll?
25. Что позволяет сделать метод join?
26. Каковы условия вызова метода wait/notify?
27. Дайте определение понятию “взаимная блокировка”.
28. Чем отличаются методы interrupt, interrupted, isInterrupted?
29. В каком случае будет выброшено исключение InterruptedException, какие методы могут его выбросить?
30. Модификаторы volatile и метод yield().
31. Пакет java.util.concurrent
32. Есть некоторый метод, который исполняет операцию i++. Переменная i типа int. Предполагается, что код будет исполнятся в многопоточной среде. Следует ли синхронизировать блок?
33. Что используется в качестве mutex, если метод объявлен static synchronized? Можно ли создавать новые экземпляры класса, пока выполняется static synchronized метод?
34. Предположим в методе run возник RuntimeException, который не был пойман. Что случится с потоком? Есть ли способ узнать о том, что Exception произошел (не заключая все тело run в блок try-catch)? Есть ли способ восстановить работу потока после того как это произошло?
35. Какие стандартные инструменты Java вы бы использовали для реализации пула потоков?
36.Что такое ThreadGroup и зачем он нужен?
37.Что такое ThreadPool и зачем он нужен?
38.Что такое ThreadPoolExecutor и зачем он нужен?
39.Что такое «атомарные типы» в Java?
40.Зачем нужен класс ThreadLocal?
41.Что такое Executor?
42.Что такое ExecutorService?
43.Зачем нужен ScheduledExecutorService?
Ответы
1. Дайте определение понятию “процесс”.
Процесс — это совокупность кода и данных, разделяющих общее виртуальное адресное пространство. Процессы изолированы друг от друга, поэтому прямой доступ к памяти чужого процесса невозможен (взаимодействие между процессами осуществляется с помощью специальных средств). Для каждого процесса ОС создает так называемое «виртуальное адресное пространство», к которому процесс имеет прямой доступ. Это пространство принадлежит процессу, содержит только его данные и находится в полном его распоряжении. Операционная система же отвечает за то, как виртуальное пространство процесса проецируется на физическую память.
Многопоточность в Java: http://habrahabr.ru/post/164487/
2. Дайте определение понятию “поток”.
Один поток («нить» или «трэд») – это одна единица исполнения кода. Каждый поток последовательно выполняет инструкции процесса, которому он принадлежит, параллельно с другими потоками этого процесса.
Thinking in Java.Параллельное выполнение. http://wikijava.it-cache.net/index.php@title=Glava_17_Thinking_in_Java_4th_edition.html
3. Дайте определение понятию “синхронизация потоков”.
Синхронизация относится к многопоточности. Синхронизированный блок кода может быть выполнен только одним потоком одновременно.
Java поддерживает несколько потоков для выполнения. Это может привести к тому, что два или более потока получат доступ к одному и тому же полю или объекту. Синхронизация — это процесс, который позволяет выполнять все параллельные потоки в программе синхронно. Синхронизация позволяет избежать ошибок согласованности памяти, вызванных непоследовательным доступом к общей памяти.
Когда метод объявлен как синхронизированный — нить держит монитор для объекта, метод которого исполняется. Если другой поток выполняет синхронизированный метод, ваш поток заблокируется до тех пор, пока другой поток не отпустит монитор.
Синхронизация достигается в Java использованием зарезервированного слова synchronized. Вы можете использовать его в своих классах определяя синхронизированные методы или блоки. Вы не сможете использовать synchronized в переменных или атрибутах в определении класса.
Синхронизация потоков, блокировка объекта и блокировка класса info.javarush.ru: http://goo.gl/gW4ONp
4. Как взаимодействуют программы, процессы и потоки?
Чаще всего одна программа состоит из одного процесса, но бывают и исключения (например, браузер Chrome создает отдельный процесс для каждой вкладки, что дает ему некоторые преимущества, вроде независимости вкладок друг от друга). В каждом процессе может быть создано множество потоков. Процессы разделены между собой (>программы), потоки в одном процессе могут взаимодействовать друг с другом (методы wait, notify, join и т.д.).
5. В каких случаях целесообразно создавать несколько потоков?
Многопоточные приложения применяются в случаях, когда можно разделить программу на несколько относительно независимых частей. В этом случае чтобы один код не ждал другой их помещают в различные потоки. В качестве примера можно привести программу с графическим интерфейсом — пока выполняются какие-либо длительные вычисления в одном потоке, интерфейс может быть доступен пользователю и не зависать, если он выполняется в другом потоке.
6. Что может произойти если два потока будут выполнять один и тот же код в программе?
Если используются не синхронизированные данные, то может произойти ситуация, когда код работает уже с устаревшими данными. Например, в первом потоке идет изменение каких-либо полей, а в это время второй поток читает эти поля.
7. Что вы знаете о главном потоке программы?
Маленькие программы на Java обычно состоят из одной нити, называемой «главной нитью» (main thread). Но программы побольше часто запускают дополнительные нити, их еще называют «дочерними нитями». Главная нить выполняет метод main и завершается. Аналогом такого метода main, для дочерних нитей служит метод run интерфейса Runnable. Много потоков — много методов main (run()).
8. Какие есть способы создания и запуска потоков?
Существует несколько способов создания и запуска потоков.
С помощью класса, реализующего Runnable
С помощью класса, расширяющего Thread
С помощью класса, реализующего java.util.concurrent.Callable
Жизненный цикл потока в Java
Эта статья направлена на объяснение различных состояний потока в мире Java. Если вы новичок в области многопоточного программирования, попробуйте сначала почитать про потоки что-нибудь базовое.
Согласно Sun Microsystems, существует четыре состояния жизненного цикла потока Java. Вот они:
Ниже дано схематическое представление жизненного цикла потока в Java:
Хороший вопрос! Планировщик — это программное обеспечение, которое используется для отслеживания задач компьютера. Он отвечает за назначение задач ресурсам, которые могут совершать работу. Мы не будем углубляться в логику, которую реализует планировщик. На данный момент достаточно знать, что планировщик имеет контроль над тем, какая задача должна быть назначена какому аппаратному ресурсу и когда, исходя из доступности ресурса и состояния задачи.
Выходит, планировщик решает, когда назначить задачу требуемому ресурсу. Но как задача переходит в нерабочее состояние? Как и почему поток должен отказаться от процессорного времени и приостановить выполнение? Это происходит по выбору или вынужденно?
Что же. Поток может находиться в нерабочем состоянии по разным причинам — иногда принудительно, иногда по собственному выбору. Вынужденные причины могут заключаться в том, что он ожидает операции ввода-вывода, например получения сообщения по порту, или он может ждать объект, который удерживается другим потоком. Последний сценарий приводит к появлению синхронизированного объекта. Когда поток обращается к синхронизированному объекту, он создает блокировку на этом объекте. Блокировка — это что-то вроде временного контракта между потоком и объектом, который дает потоку эксклюзивный доступ к объекту, запрещая доступ любому другому потоку. Для обеспечения этого контракта Java связывает с каждым объектом монитор. Поток также может быть перемещен планировщиком в нерабочее состояние (спящий режим) на основе логики совместного использования ресурсов планировщика.
Потоки могут перейти в нерабочее состояние по выбору. То есть по выбору программиста. Программист может написать метод потока ( run или любой другой метод, который вызывается внутри run ) таким образом, чтобы тот намеренно уступал процессорное время. Так делается, чтобы получить максимальную отдачу от доступных вычислительных мощностей или вызвать задержки после выполнения определенной части потока. Давайте посмотрим, какие методы добровольного отказа от процессорного времени нам доступны:
Время запачкать руки! Напишем-ка небольшой код, чтобы создать пару потоков и проверить их состояние на протяжении всего выполнения.
Простое выполнение единственного потока
Этот код выведет следующее:
Давайте попробуем понять, что здесь происходит.
2. Выполнение нескольких потоков
1. Влияние синхронизации
На рис. 3 показан вывод без ключевого слова synchronized, а на рис. 4 — с ключевым словом synchronized.
Почему вывод такой рандомный? 🤔
А поскольку мы использовали ключевое слово synchronized для получения вывода на рис.4, то первый поток, получивший доступ к объекту, удерживал его блокировку. Это преграждало другому потоку доступ к объекту до завершения выполнения первого потока.
2. Методы “sleep” и “yield”
3. Методы “wait” и “notify”
Что делать, если мы не хотим, чтобы поток заканчивал выполнение, пока какой-то другой поток не скажет ему продолжить?
Допустим, у нас есть учетная запись у поставщика широкополосных сетевых услуг и мы хотим добавить на баланс еще данных. Если у нашей учетной записи недостаточно средств, мы не сможем пополнить счет для передачи данных. Таким образом, поставщик услуг будет ждать, пока мы добавим на аккаунт средства, чтобы завершить дозакупку. Давайте взглянем на код.
4.Метод “join”
Метод join используется, когда мы хотим остановить выполнение программы до тех пор, пока вызывающий поток не завершит выполнение. Можете ли вы представить себе такой сценарий?
Вывод без join :
Person [name=Rajat, job=null, address=Dublin]
Вывод с join :
Person [name=Rajat, job=Software Developer, address=Dublin]
Занимаясь многопоточным программированием, я провел много лет в поисках идеального способа реализации, и это было трудно. Итак, я попытался объяснить вам многопоточное программирование. Рекомендую поиграть с кодом из этой статьи, чтобы исследовать его глубже и укрепить свое понимание предмета.
Многопоточность в Java: работа с потоками и полезные методы класса Thread
Многопоточность в Java — это одновременное выполнение двух или более потоков для максимального использования центрального процессора (CPU — central processing unit). Каждый поток работает параллельно и не требует отдельной области памяти. К тому же, переключение контекста между потоками занимает меньше времени.
Процессы в Java: определение и функции
Что такое потоки
Поток — наименьшее составляющее процесса. Потоки могут выполняться параллельно друг с другом. Их также часто называют легковесными процессами. Они используют адресное пространство процесса и делят его с другими потоками.
Потоки могут контролироваться друг друга и общаться посредством методов wait(), notify(), notifyAll().
Состояния потоков
Потоки могут пребывать в нескольких состояниях:
Способы запуска потоков
Приложение, создающее экземпляр класса Thread, должно предоставить код, который будет работать в этом потоке. Существует два способа, чтобы добиться этого:
Обратите внимание, что оба примера вызывают Thread.start, чтобы запустить новый поток.
Какой из способов выбрать? Первый — с использованием объекта Runnable — более общий, потому что этот объект может превратить отличный от Thread класс в подкласс. Этот способ более гибкий и может использоваться для высокоуровневых API управления потоками.
Второй способ больше подходит для простых приложений, но есть условие: класс задачи должен быть потомком Thread.
Завершение процесса и потоки-демоны
В Java процесс завершается тогда, когда завершаются все его основные и дочерние потоки.
Потоки-демоны — это низкоприоритетные потоки, работающие в фоновом режиме для выполнения таких задач, как сбор «мусора»: они освобождают память неиспользованных объектов и очищают кэш. Большинство потоков JVM (Java Virtual Machine) являются потоками-демонами.
Свойства потоков-демонов:
Чтобы установить, является ли поток демоном, используется метод boolean isDaemon(). Если да, то он возвращает значение true, если нет, то — то значение false.
Завершение потоков
Завершение потока Java требует подготовки кода реализации потока. Класс Java Thread содержит метод stop(), но он помечен как deprecated. Оригинальный метод stop() не дает никаких гарантий относительно состояния, в котором поток остановили. То есть, все объекты Java, к которым у потока был доступ во время выполнения, останутся в неизвестном состоянии. Если другие потоки в приложении имели доступ к тем же объектам, то они могут неожиданно «сломаться».
Вместо вызова метода stop() нужно реализовать код потока, чтобы его остановить. Приведем пример класса с реализацией Runnable, который содержит дополнительный метод doStop(), посылающий Runnable сигнал остановиться. Runnable проверит его и остановит, когда будет готов.
Обратите внимание на методы doStop() и keepRunning(). Вызов doStop() происходит не из потока, выполняющего метод run() в MyRunnable.
Метод keepRunning() вызывается внутренней потоком, выполняющим метод run() MyRunnable. Поскольку метод doStop() не вызван, метод keepRunning() возвратит значение true, то есть поток, выполняющий метод run(), продолжит работать.
В примере сначала создается MyRunnable, а затем передается потоку и запускает его. Поток, выполняющий метод main() (главный поток), засыпает на 10 секунд и потом вызывает метод doStop() экземпляра класса MyRunnable. Впоследствии поток, выполняющий метод MyRunnable, остановится, потому что после того, как вызван doStop(), keepRunning() возвратит false.
Обратите внимание, если для реализация Runnable нужен не только метод run() (а например, еще метод stop() или pause()), реализацию Runnable больше нельзя будет создать с помощью лямбда-выражений. Понадобится кастомный класс или интерфейс, расширяющий Runnable, который содержит дополнительные методы и реализуется анонимным классом.
Метод Thread.sleep()
Поток может остановиться сам, вызвав статический метод Thread.sleep(). Thread.sleep() принимает в качестве параметра количество миллисекунд. Метод sleep() попытается заснуть на это количество времени перед возобновлениям выполнения. Thread sleep() не гарантирует абсолютной точности.
Приведем пример остановки потока Java на 10 секунд (10 тысяч миллисекунд) с помощью вызова метода Thread sleep():
Поток, выполняющий код, уснет примерно на 10 секунд.
Метод yield()
Предотвратить выполнение потока можно методом yield(): предположим, существует три потока t1, t2, and t3. Поток t1 выполняется процессором, а потоки t2 и t3 находятся в состоянии Ready/Runnable. Время выполнения для потока t1 — 5 часов, а для t2 – 5 минут.
Поскольку t1 закончит свое выполнение через 5 часов, t2 придется ждать все это время, чтобы закончить 5-минутную задачу. В таких случаях, когда один поток требует слишком много времени, чтобы завершить свое выполнение, нужен способ приостановить выполнение длинного потока в промежутке, если какая-то важная задача не завершена. Тут и поможет yield ().
По сути, yield() означает, что поток не выполняет ничего особо важного, и если другие потоки или процессы требуют запуска, то их можно запустить.
Использование метода yield() :
Синтаксис:
Результат:
Метод join()
Метод join() экземпляра класса Thread используется для объединения начала выполнения одного потока с завершением выполнения другого потока. Это необходимо, чтобы один поток не начал выполняться до того, как завершится другой поток. Если метод join() вызывается на Thread, то выполняющийся в этот момент поток блокируется до момента, пока Thread не закончит выполнение.
Метод join() ждет не более указанного количества миллисекунд, пока поток умрет. Тайм-аут 0 (ноль) означает «ждать вечно».
Синтаксис:
Например:
Результат:
Из примера видно, что как только поток t1 завершает выполнение задачи, потоки t2 и t3 начинают выполнять свои задачи.
Приоритеты потоков
У каждого потока есть приоритет. Приоритет обозначается числом от 1 до 10. В большинстве случаев планировщик распределяет потоки относительно их приоритета (другими словами — происходит приоритетное планирование). Но это не гарантированно, поскольку он зависит от того, какое планирование выберет JVM.
Три константы, которые определяются в классе Thread:
1. public static int MIN_PRIORITY (значение равно 1);
2. public static int NORM_PRIORITY (дефолтный приоритет потока);
3. public static int MAX_PRIORITY (значение равно 10).
Пример приоритета потока:
Результат:
Некоторые полезные методы класса Thread
Имя потока – ассоциированная с ним строка, которая в некоторых случаях помогает понять, какой поток выполняет действие.
Многопоточность в Java
Здравствуйте! В этой статье я вкратце расскажу вам о процессах, потоках, и об основах многопоточного программирования на языке Java.
Наиболее очевидная область применения многопоточности – это программирование интерфейсов. Многопоточность незаменима тогда, когда необходимо, чтобы графический интерфейс продолжал отзываться на действия пользователя во время выполнения некоторой обработки информации. Например, поток, отвечающий за интерфейс, может ждать завершения другого потока, загружающего файл из интернета, и в это время выводить некоторую анимацию или обновлять прогресс-бар. Кроме того он может остановить поток загружающий файл, если была нажата кнопка «отмена».
Еще одна популярная и, пожалуй, одна из самых хардкорных областей применения многопоточности – игры. В играх различные потоки могут отвечать за работу с сетью, анимацию, расчет физики и т.п.
Давайте начнем. Сначала о процессах.
Процессы
Процесс — это совокупность кода и данных, разделяющих общее виртуальное адресное пространство. Чаще всего одна программа состоит из одного процесса, но бывают и исключения (например, браузер Chrome создает отдельный процесс для каждой вкладки, что дает ему некоторые преимущества, вроде независимости вкладок друг от друга). Процессы изолированы друг от друга, поэтому прямой доступ к памяти чужого процесса невозможен (взаимодействие между процессами осуществляется с помощью специальных средств).
Для каждого процесса ОС создает так называемое «виртуальное адресное пространство», к которому процесс имеет прямой доступ. Это пространство принадлежит процессу, содержит только его данные и находится в полном его распоряжении. Операционная система же отвечает за то, как виртуальное пространство процесса проецируется на физическую память.
Схема этого взаимодействия представлена на картинке. Операционная система оперирует так называемыми страницами памяти, которые представляют собой просто область определенного фиксированного размера. Если процессу становится недостаточно памяти, система выделяет ему дополнительные страницы из физической памяти. Страницы виртуальной памяти могут проецироваться на физическую память в произвольном порядке.
При запуске программы операционная система создает процесс, загружая в его адресное пространство код и данные программы, а затем запускает главный поток созданного процесса.
Потоки
Один поток – это одна единица исполнения кода. Каждый поток последовательно выполняет инструкции процесса, которому он принадлежит, параллельно с другими потоками этого процесса.
Следует отдельно обговорить фразу «параллельно с другими потоками». Известно, что на одно ядро процессора, в каждый момент времени, приходится одна единица исполнения. То есть одноядерный процессор может обрабатывать команды только последовательно, по одной за раз (в упрощенном случае). Однако запуск нескольких параллельных потоков возможен и в системах с одноядерными процессорами. В этом случае система будет периодически переключаться между потоками, поочередно давая выполняться то одному, то другому потоку. Такая схема называется псевдо-параллелизмом. Система запоминает состояние (контекст) каждого потока, перед тем как переключиться на другой поток, и восстанавливает его по возвращению к выполнению потока. В контекст потока входят такие параметры, как стек, набор значений регистров процессора, адрес исполняемой команды и прочее…
Проще говоря, при псевдопараллельном выполнении потоков процессор мечется между выполнением нескольких потоков, выполняя по очереди часть каждого из них.
Вот как это выглядит:
Цветные квадраты на рисунке – это инструкции процессора (зеленые – инструкции главного потока, синие – побочного). Выполнение идет слева направо. После запуска побочного потока его инструкции начинают выполняться вперемешку с инструкциями главного потока. Кол-во выполняемых инструкций за каждый подход не определено.
То, что инструкции параллельных потоков выполняются вперемешку, в некоторых случаях может привести к конфликтам доступа к данным. Проблемам взаимодействия потоков будет посвящена следующая статья, а пока о том, как запускаются потоки в Java…
Запуск потоков
Каждый процесс имеет хотя бы один выполняющийся поток. Тот поток, с которого начинается выполнение программы, называется главным. В языке Java, после создания процесса, выполнение главного потока начинается с метода main(). Затем, по мере необходимости, в заданных программистом местах, и при выполнении заданных им же условий, запускаются другие, побочные потоки.
В языке Java поток представляется в виде объекта-потомка класса Thread. Этот класс инкапсулирует стандартные механизмы работы с потоком.
Запустить новый поток можно двумя способами:
Способ 1
Создать объект класса Thread, передав ему в конструкторе нечто, реализующее интерфейс Runnable. Этот интерфейс содержит метод run(), который будет выполняться в новом потоке. Поток закончит выполнение, когда завершится его метод run().
Для пущего укорочения кода можно передать в конструктор класса Thread объект безымянного внутреннего класса, реализующего интерфейс Runnable:
Способ 2
Создать потомка класса Thread и переопределить его метод run():
В приведённом выше примере в методе main() создается и запускается еще один поток. Важно отметить, что после вызова метода mSecondThread.start() главный поток продолжает своё выполнение, не дожидаясь пока порожденный им поток завершится. И те инструкции, которые идут после вызова метода start(), будут выполнены параллельно с инструкциями потока mSecondThread.
Для демонстрации параллельной работы потоков давайте рассмотрим программу, в которой два потока спорят на предмет философского вопроса «что было раньше, яйцо или курица?». Главный поток уверен, что первой была курица, о чем он и будет сообщать каждую секунду. Второй же поток раз в секунду будет опровергать своего оппонента. Всего спор продлится 5 секунд. Победит тот поток, который последним изречет свой ответ на этот, без сомнения, животрепещущий философский вопрос. В примере используются средства, о которых пока не было сказано (isAlive() sleep() и join()). К ним даны комментарии, а более подробно они будут разобраны дальше.
В приведенном примере два потока параллельно в течении 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.
Взаимодействовать с потоком можно с помощью метода 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 после добавления поддержки прерываний:
Как видите, мы избавились от метода finish() и реализовали тот же механизм завершения потока с помощью встроенной системы прерываний. В этой реализации мы получили одно преимущество — метод sleep() вернет управление (сгенерирует исключение) незамедлительно после прерывания потока.
Заметьте что методы sleep() и join() обёрнуты в конструкции try-catch. Это необходимое условие работы этих методов. Вызывающий их код должен перехватывать исключение InterruptedException, которое они бросают при прерывании во время ожидания.
С запуском и завершением потоков разобрались, дальше я расскажу о методах, использующихся при работе с потоками.
Метод Thread.sleep()
Thread.sleep() — статический метод класса Thread, который приостанавливает выполнение потока, в котором он был вызван. Во время выполнения метода sleep() система перестает выделять потоку процессорное время, распределяя его между другими потоками. Метод sleep() может выполняться либо заданное кол-во времени (миллисекунды или наносекунды) либо до тех пор пока он не будет остановлен прерыванием (в этом случае он сгенерирует исключение InterruptedException).
Несмотря на то, что метод sleep() может принимать в качестве времени ожидания наносекунды, не стоит принимать это всерьез. Во многих системах время ожидания все равно округляется до миллисекунд а то и до их десятков.
Метод yield()
Статический метод Thread.yield() заставляет процессор переключиться на обработку других потоков системы. Метод может быть полезным, например, когда поток ожидает наступления какого-либо события и необходимо чтобы проверка его наступления происходила как можно чаще. В этом случае можно поместить проверку события и метод Thread.yield() в цикл:
Метод join()
В Java предусмотрен механизм, позволяющий одному потоку ждать завершения выполнения другого. Для этого используется метод join(). Например, чтобы главный поток подождал завершения побочного потока myThready, необходимо выполнить инструкцию myThready.join() в главном потоке. Как только поток myThready завершится, метод join() вернет управление, и главный поток сможет продолжить выполнение.
Метод join() имеет перегруженную версию, которая получает в качестве параметра время ожидания. В этом случае join() возвращает управление либо когда завершится ожидаемый поток, либо когда закончится время ожидания. Подобно методу Thread.sleep() метод join может ждать в течение миллисекунд и наносекунд – аргументы те же.
С помощью задания времени ожидания потока можно, например, выполнять обновление анимированной картинки пока главный (или любой другой) поток ждёт завершения побочного потока, выполняющего ресурсоёмкие операции:
В этом примере поток 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. Если эта статья окажется полезной, то в следующей я расскажу о проблемах совместного доступа потоков к ресурсам и о методах их решения.