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

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

Процесс в программном обеспечении:

· Экземпляр программы называется процессом. (например, MS Word и MS Paint - это два разных процесса)

· Процесс хранится в оперативной памяти

· Процессы изолированы и не делят между собой память.

· Процесс может содержать несколько потоков

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

· Одно приложение Java выполняется в одном процессе

Врезка в программное обеспечение:

· Поток - это легкое подмножество процесса. (Редактирование и автосохранение данных в MS Word (один процесс) выполняется двумя отдельными потоками.)

· Потоки являются частью процессов и остаются в них.

· Несколько потоков могут совместно использовать память между собой.

· Каждый процесс содержит минимум один поток.

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

· Одно приложение Java может иметь несколько потоков.

Поток / многопоточность и их требования:

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

Основы темы:

В Java можно определить поток двумя способами.

1. Экземпляр класса java.lang.Thread или класса, реализующего интерфейс Runnable.

2. Поток исполнения

Экземпляр потока - это, по сути, простой объект Java, как и любые другие объекты, имеющие свойства, унаследованные от класса Thread. У него есть переменные, методы и остаются в куче.

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

Один интересный факт: независимо от того, создает ли разработчик поток, по крайней мере, один поток всегда выполняется в фоновом режиме, когда выполняется программа Java. Метод main () - это Java, которая инициирует программу, которая сама создает стек вызовов, и, конечно же, метод main () является первым методом в стеке.

Каждый раз, когда создается новый поток, создается новый «стек вызовов». Все методы нового потока остаются в новом стеке вызовов. Каждый созданный стек вызовов запускается параллельно, что позволяет программе выполнять несколько задач одновременно. JVM отвечает за планирование работы нескольких потоков. JVM отличается от одного к другому. Механизм планирования потоков варьируется от одной JVM к другой. Как и JVM, продаваемая Oracle, и JVM с открытым исходным кодом могут различаться по реализации. В некоторых случаях JVM передает планирование основной операционной системе –OS. Поэтому, когда речь идет о потоках, можно гарантировать очень мало вещей. Одна и та же программа может давать разные результаты при запуске на разных машинах. Поэтому рекомендуется, чтобы программа, написанная с обработкой потоков, не зависела от одной конкретной JVM. Различные JVM могут запускать потоки совершенно разными способами, что приводит к разному результату. Некоторые могут предложить каждому потоку равные шансы, некоторые могут подождать, пока один из них не закончится, тогда только любой другой поток получит шансы на выполнение. Разработчики должны быть осторожны, чтобы написать хорошее и безопасное многопоточное приложение. Также «поток демона» и «поток пользователя» не совпадают.

Создание темы:

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

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

1 ›Расширяя класс java.lang.Thread

2 ›Реализация интерфейса Runnable (в основном java.lang.Thread внутренне реализует Runnable)

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

Первый будет выглядеть так:

Второй подход будет выглядеть так:

Когда оба метода запуска будут вызваны правильно, они будут давать одинаковый результат при запуске в отдельном потоке. Но, следуя принципам OOPS, рекомендуется использовать интерфейс Runnable, а не наследование, главным образом по следующим причинам:

· Наследование или подклассы должны быть зарезервированы для специализированной версии более обобщенного суперкласса.

· Если в классе широко не используется специализированная конкретная версия Thread, реализующая интерфейс Runnable, позволяет классу расширять или реализовывать другие классы.

· Расширение класса Thread рекомендуется, когда пользовательский класс Thread требуется для определенного конкретного поведения.

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

Создание темы:

Проще говоря, для выполнения потока требуется экземпляр класса Thread. Независимо от того, создается ли пользовательский класс Thread путем расширения java.lang.Thread или путем реализации интерфейса Runnable, требуется объект Thread (worker) для вызова метода run () (задание, которое необходимо выполнить).

Когда пользовательский класс Thread создается путем расширения класса Thread, получить объект Thread относительно просто. Например,

С помощью этой ссылочной переменной t теперь мы можем вызвать метод run (), который создаст новый поток и свой собственный стек вызовов.

Когда пользовательский класс Thread создается путем реализации интерфейса Runnable, создание экземпляра немного отличается.

Для запуска отдельного потока по-прежнему требуется экземпляр потока (worker). Но в этом случае метод run () (работа, которую необходимо выполнить) - это не класс Thread, а класс, реализующий интерфейс Runnable. Здесь нам все еще нужно создать экземпляр Thread, но передав объект интерфейса Runnable в конструктор, чтобы созданный поток знал, какой метод run () вызывать. (Рабочий должен знать, что работа должна быть выполнена). Runnable, переданный конструктору Thread, известен как «target» или «target’ Runnable. Код для выполнения будет таким, как показано ниже.

Теперь экземпляр Thread t знает, что он должен выполнить метод run (), реализованный в интерфейсе Runnable. Если цель не передана, поток выполнит свой собственный метод run ().

Мы также можем передать одинаковые или единственные целевые объекты Runnable нескольким потокам, чтобы все экземпляры потоков выполняли одну и ту же работу.

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

· Нить()

· Поток (работоспособная цель)

· Поток (Runnable target, String name)

· Тема (название строки)

Как мы поняли, последние два конструктора принимают объекты String, которые дают определяемые пользователем имена созданным потокам. Главный поток по умолчанию имеет имя «main». Если имя не задано, Java автоматически дает имена потокам как Thread-serial- integer-of-thread-creation

например, ” Thread-1”;

Начало обсуждения:

Пока что все очень похоже на другие вещи Java. Но для создания потока нам нужен новый стек вызовов, и, как обсуждалось ранее, вызов метода run () делает это. Но предположим, что у нас есть экземпляр Thread t, в котором есть цель r, т.е. доступен метод run ().

Мы можем просто вызвать метод run, вызвав t.run () правильно, как и любой другой код Java. Но он ничего не сделает с созданием и запуском нового потока. Он обязательно выполнит команду запуска, но в том же потоке, а не в новом потоке. Чтобы создать новый поток со своим собственным стеком вызовов, нам нужно выполнить следующую команду. Нам нужно вызвать метод start () для объекта Thread.

Когда мы вызываем start () для объекта Thread, объект Thread создает новый поток и свой собственный стек вызовов, вызывая метод run (). Перед вызовом метода start () для объекта Thread это просто еще один простой объект Java, а не новый поток выполнения. Когда метод start () вызывается для объекта потока, происходит следующее.

· Запущен новый поток выполнения, т.е. создается новый стек вызовов.

· Поток переходит из состояния NEW в состояние RUNNABLE. (состояния потока обсуждаются позже)

· Когда поток получает возможность выполнить целевой метод run (), он будет запущен.

· Метод start () находится в классе Thread.

· Метод run находится в интерфейсе Runnable, но поскольку внутри Thread реализует runnable, он также доступен в классе Thread.

Ниже приведено наглядное описание того, как работают потоки и стек вызовов.

Рисунок 1: Когда запускается программа Java.

Рисунок 2: Когда метод start () вызывается для объекта потока

Запуск и запуск нескольких потоков:

Прежде чем мы углубимся в эту тему, мы должны помнить, что в Thread не все гарантируется.

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

· Работа нескольких потоков не гарантируется, если у них одинаковые приоритеты.

· Тема не следует порядку, в котором они закодированы.

· Нет никакой гарантии, что после вызова метода start () он будет продолжать работу до тех пор, пока его работа не будет выполнена.

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

· После завершения метода run () поток считается в состоянии DEAD, его стек вызовов растворяется.

· Объект Thread может оставаться в состоянии DEAD, а поток выполнения - нет.

· После завершения потока start () не может быть снова вызван поверх него. Это до исключения исключения.

· Многократный вызов start () для объекта Thread вызывает исключение. Объект Thread может вызвать start () только один раз.

· Только поток в состоянии NEW может вызывать метод start ().

· Поток, который уже находится в состоянии RUNNABLE или DEAD, не может быть перезапущен.

· Каждый поток гарантированно будет выполняться, пока он не будет завершен.

· В рамках одного потока действия гарантированно выполняются в соответствии с кодом. Но между разными потоками они могут непредсказуемо перемешиваться.

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

Однако одно можно сделать гарантированным. Мы можем вызвать start () для объекта Thread и сказать, что он не запускается, пока не завершится какой-либо другой поток, т.е. метод run () завершит свою работу, используя метод join ().

Давайте проверим приведенный ниже код и посмотрим, как один и тот же код выполнялся на одной машине несколько раз и давал разные результаты.

Многопоточный код:

Выводится после выполнения одной и той же программы на одном компьютере несколько раз.

Состояния потока и переход:

Объект Thread может иметь одно из 5 различных состояний. Вот они:

1 ›НОВИНКА

2 ›РАБОТАЮЩИЙ

3 ›РАБОТАЕТ

4 ›ОЖИДАНИЕ / БЛОКИРОВКА / СПЯЩИЙ

5 ›УМЕР / ПРЕКРАЩЕН

Состояние NEW: когда создается новый объект потока, он считается находящимся в состоянии NEW перед вызовом метода start (). На данный момент экземпляр активен, но поток выполнения не считается живым. Поток, однажды находившийся в состоянии NEW, если он был переведен в другое состояние, не может снова вернуться в это состояние.

Состояние RUNNABLE: когда метод start () вызывается для объекта Thread, объект переходит в состояние RUNNABLE. В этом состоянии объект Thread может быть запущен, но еще не находится в рабочем состоянии, или планировщик не выбрал поток для выполнения. В этом состоянии поток считается живым.

Поток может вернуться в это состояние из состояния NEW или из других состояний, таких как RUNNING или WAITING / BLOCKING / SLEEPING, но не после того, как он был завершен и перешел в состояние DEAD, невозможно вернуться в это состояние.

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

Состояние RUNNING: это состояние, когда поток фактически выполняет свои действия. Это происходит, когда планировщик выбирает поток из пула потоков и позволяет ему выполняться. На этом этапе поток может вернуться либо в состояние RUNNABLE, либо в состояние WAITING / BLOCKING / SLEEPING, либо перейти в состояние DEAD, если он выполнил свою задачу. Но чтобы вернуться в это состояние, поток должен быть в состоянии RUNNABLE. Планировщик решает переместить поток в это состояние. Конечно, на данном этапе поток состояний активен.

Состояние WAITING / BLOCKING / SLEEPING: на этом этапе поток не может быть запущен и не находится в состоянии RUNNABLE. Но он может снова вернуться в состояние RUNNABLE. В этом конкретном состоянии мы видим, что 3 варианта объединены вместе. Но одно все еще распространено: поток все еще жив.

· ОЖИДАНИЕ: код (не другой поток, что невозможно) может указывать потоку на ожидание. В этом случае другой поток может отправить уведомление этому потоку, чтобы он больше не ждал, и текущий поток возвращается в состояние RUNNABLE.

В Javadoc есть еще один этап в этом состоянии, а именно TIMED_WAITING, когда поток, который ожидает, пока другой поток выполнит действие, ожидает в течение указанного времени ожидания в этом состоянии.

· БЛОКИРОВКА: поток может извлекать некоторый ресурс, поэтому он заблокирован. Например, он ожидает завершения некоторой операции ввода-вывода или записи некоторых данных в файл или базу данных. Как только ресурс становится доступным, он возвращается в состояние RUNNABLE.

· SLEEPING: если код указывает потоку на некоторое время спать, он остается в этом состоянии. Он используется в коде для замедления потока или для того, чтобы дать другим потокам некоторые шансы, поскольку разумная очередность не гарантируется и не обрабатывается планировщиком. По истечении периода времени он возвращается в состояние РАБОТАЮЩИЙ.

Состояние DEAD / TERMINATED: поток считается мертвым, когда его метод run () завершается. Объект Thread может оставаться в куче, но поток выполнения завершен. Этот поток нельзя запустить снова, вызвав функцию start (), так как он вызовет исключение. На этом этапе поток, конечно, не считается живым.

Приоритет темы:

Каждый поток имеет приоритет и получает свою очередь в зависимости от него (в основном). Идентификатор приоритета, определяемый целочисленными значениями, обычно от 1 до 10, но в некоторых случаях может быть от 1 до 5. В большинстве случаев планировщик JVM использует методы квантования времени, где каждому потоку дается изрядное количество времени. Но это не гарантируется для всех JVM. Некоторые JVM могут позволить планировщику позволить одному потоку завершиться перед выполнением другого потока.

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

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

Настройка приоритетов потоков:

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

Поток с тем же значением, что и основной поток

Поток t теперь будет иметь приоритет 8

Важные методы / шаги для предотвращения выполнения потоков:

· Sleep (): Гарантированно предотвращает выполнение потока в течение указанного периода времени.

· Join (): гарантированно останавливает выполнение текущего потока до тех пор, пока поток, к которому он присоединяется, не завершится или не завершит выполнение.

· Yield (): не гарантирует полезного эффекта. Но заставляет текущий работающий поток вернуться в состояние RUNNABLE из выполнения, так что другие потоки в пуле могут иметь возможность работать.

· Вызов метода wait () для объекта. (Будет объяснено в Части 2.)

· Планировщик потоков может решить остановить выполнение текущего потока обратно в пул. Это зависит от самого планировщика и его выбора.

· Когда поток завершает свой метод run ().

Я благодарен:

Прежде всего мои родители, семья и друзья

1 ›Книга SCJP Кэти Сьерра и Берта Бейтса

2 › https://docs.oracle.com/javase/7/docs/api/

3 › https://www.stackoverflow.com

4 › https://www.quora.com

5 ›Мои работодатели и проекты, которые они мне поручили.