Завершение процесса и демоны
The Government of the Russian Federation
The Federal State Autonomous Institution of Higher Education "National Research University - Higher School of Economics"
Moscow institute of electronics and mathematics of National Research University «Higher School of Economics»
Faculty of Information Technology and Computer Engineering
Department of Computer Systems and Networks
Course title: Network computing
Term paper on the topic:
«Funds for the implementation of Java multithreaded architecture»
Tutor: | Baybikova T.N. |
Student: Group: | Vasin A.A. SVBS-51 |
Moscow 2014
Оглавление
Постановка задачи | |
Введение | |
1. Теоретическая часть (обзор средств и методов реализации многопоточности в Java). | |
1.1 Процессы | |
1.2 Потоки | |
1.3 Запуск потоков | |
1.4 Завершение процесса и демоны | |
1.5 Завершение потоков | |
2. Методы использующиеся при работе с потоками. | |
2.1. Метод Thread.sleep() | |
2.2. Метод yield() | |
2.3. Метод join() | |
3. Приоритеты потоков | |
4. Проблемы при реализации параллельных программ | |
4.1 Взаимная блокировка (deadlock) | |
4.2. Голодание (starvation). | |
4.3. Активная блокировка (livelock) . | |
Заключение теоретической части. | |
Алгоритм работы программы. | |
Листинг программы (Приложение 1.) | |
Анализ выполнения программы (Приложение 2.) | |
Заключение | |
Список использованных источников и литературы |
Постановка задачи
Тема курсовой работы:«Средства Java для реализации многопоточной архитектуры».
Цель работы: Используя средства многопоточной архитектуры произвести вычисления и произвести замер скорости выполнения вычисления для разного количества потоков. Для вычисления использовать:
Функцию:
Количество потоков: любое;
Количество аргументов: два (x,y);
Создать интерфейс ввода и вывода результата;
Для реализации данной задачи используем следующий план действий:
- Проведем обзор средств и методов реализации многопоточности в языке Java/
- Построим алгоритм программы.
- Напишем по алгоритму сам текст программы.
- На примере использования нескольких потоков проанализируем результат.
- Запишем вывод о проделанной работе.
Введение
Наиболее очевидная область применения многопоточности – это программирование интерфейсов. Многопоточность незаменима тогда, когда необходимо, чтобы графический интерфейс продолжал отзываться на действия пользователя во время выполнения некоторой обработки информации. Например, поток, отвечающий за интерфейс, может ждать завершения другого потока, загружающего файл из интернета, и в это время выводить некоторую анимацию или обновлять прогресс-бар. Кроме того он может остановить поток загружающий файл, если была нажата кнопка «отмена».
Еще одна популярная и, пожалуй, одна из самых востребованных областей применения многопоточности – игры. В играх различные потоки могут отвечать за работу с сетью, анимацию, расчет физики и т.п. Для полного понимания приведем далее основные понятия и примеры.
Теоретическая часть (обзор средств и методов реализации многопоточности в Java).
Процессы
Процесс — это совокупность кода и данных, разделяющих общее виртуальное адресное пространство. Чаще всего одна программа состоит из одного процесса, но бывают и исключения (например, браузер Chrome создает отдельный процесс для каждой вкладки, что дает ему некоторые преимущества, вроде независимости вкладок друг от друга). Процессы изолированы друг от друга, поэтому прямой доступ к памяти чужого процесса невозможен (взаимодействие между процессами осуществляется с помощью специальных средств).
Для каждого процесса ОС создает так называемое «виртуальное адресное пространство», к которому процесс имеет прямой доступ. Это пространство принадлежит процессу, содержит только его данные и находится в полном его распоряжении. Операционная система же отвечает за то, как виртуальное пространство процесса проецируется на физическую память.
Схема этого взаимодействия представлена на рис. 1. Операционная система оперирует так называемыми страницами памяти, которые представляют собой просто область определенного фиксированного размера. Если процессу становится недостаточно памяти, система выделяет ему дополнительные страницы из физической памяти. Страницы виртуальной памяти могут проецироваться на физическую память в произвольном порядке.
Рис.1
При запуске программы операционная система создает процесс, загружая в его адресное пространство код и данные программы, а затем запускает главный поток созданного процесса.
Потоки
Один поток – это одна единица исполнения кода. Каждый поток последовательно выполняет инструкции процесса, которому он принадлежит, параллельно с другими потоками этого процесса.
Следует отдельно обговорить фразу «параллельно с другими потоками». Известно, что на одно ядро процессора, в каждый момент времени, приходится одна единица исполнения. То есть одноядерный процессор может обрабатывать команды только последовательно, по одной за раз (в упрощенном случае). Однако запуск нескольких параллельных потоков возможен и в системах с одноядерными процессорами. В этом случае система будет периодически переключаться между потоками, поочередно давая выполняться то одному, то другому потоку. Такая схема называется псевдо-параллелизмом. Система запоминает состояние (контекст) каждого потока, перед тем как переключиться на другой поток, и восстанавливает его по возвращению к выполнению потока. В контекст потока входят такие параметры, как стек, набор значений регистров процессора, адрес исполняемой команды и т.д.
Проще говоря, при псевдопараллельном выполнении потоков процессор мечется между выполнением нескольких потоков, выполняя по очереди часть каждого из них (рис.2).
Рис.2
Цветные квадраты на рисунке – это инструкции процессора (зеленые – инструкции главного потока, синие – побочного). Выполнение идет слева направо. После запуска побочного потока его инструкции начинают выполняться вперемешку с инструкциями главного потока. Количество выполняемых инструкций за каждый подход не определено.
То, что инструкции параллельных потоков выполняются вперемешку, в некоторых случаях может привести к конфликтам доступа к данным. Проблемам взаимодействия потоков будет посвящена следующая статья, а пока о том, как запускаются потоки в Java.
Запуск потоков
Каждый процесс имеет хотя бы один выполняющийся поток. Тот поток, с которого начинается выполнение программы, называется главным. В языке Java, после создания процесса, выполнение главного потока начинается с метода main(). Затем, по мере необходимости, в заданных программистом местах, и при выполнении заданных им же условий, запускаются другие, побочные потоки.
В языке Java поток представляется в виде объекта-потомка класса Thread. Этот класс инкапсулирует стандартные механизмы работы с потоком.
Запустить новый поток можно двумя способами:
Способ 1
Создать объект класса Thread, передав ему в конструкторе нечто, реализующее интерфейс Runnable. Этот интерфейс содержит метод run(), который будет выполняться в новом потоке. Поток закончит выполнение, когда завершится его метод run().
Выглядит это так (Пример 1.):
class SomeThing //Нечто, реализующее интерфейс Runnable
implements Runnable //(содержащее метод run())
{
public void run() //Этот метод будет выполняться в побочном потоке
{
System.out.println("Привет из побочного потока!");
}
}
public class Program //Класс с методом main()
{
static SomeThing mThing; //mThing - объект класса, реализующего интерфейс Runnable
public static void main(String[] args)
{
mThing = new SomeThing();
Thread myThready = new Thread(mThing); //Создание потока "myThready"
myThready.start(); //Запуск потока
System.out.println("Главный поток завершён...");
}
}
Для пущего укорочения кода можно передать в конструктор класса Thread объект безымянного внутреннего класса, реализующего интерфейс Runnable:
Пример 2.
public class Program //Класс с методом main().
{
public static void main(String[] args)
{
//Создание потока
Thread myThready = new Thread(new Runnable()
{
public void run() //Этот метод будет выполняться в побочном потоке
{
System.out.println("Привет из побочного потока!");
}
});
myThready.start(); //Запуск потока
System.out.println("Главный поток завершён...");
}
}
Способ 2(Пример3.)
Создать потомка класса Thread и переопределить его метод run():
class AffableThread extends Thread
{
@Override
public void run() //Этот метод будет выполнен в побочном потоке
{
System.out.println("Привет из побочного потока!");
}
}
public class Program
{
static AffableThread mSecondThread;
public static void main(String[] args)
{
mSecondThread = new AffableThread(); //Создание потока
mSecondThread.start(); //Запуск потока
System.out.println("Главный поток завершён...");
}
}
В приведённом выше примере в методе main() создается и запускается еще один поток. Важно отметить, что после вызова метода mSecondThread.start() главный поток продолжает своё выполнение, не дожидаясь пока порожденный им поток завершится. И те инструкции, которые идут после вызова метода start(), будут выполнены параллельно с инструкциями потока mSecondThread.
Для демонстрации параллельной работы потоков рассмотрим программу, в которой два потока спорят на предмет философского вопроса «что было раньше, яйцо или курица?». Главный поток уверен, что первой была курица, о чем он и будет сообщать каждую секунду. Второй же поток раз в секунду будет опровергать своего оппонента. Всего спор продлится 5 секунд. Победит тот поток, который последним изречет свой ответ на этот, без сомнения, животрепещущий философский вопрос. В примере используются средства, (isAlive() sleep() и join()). К ним даны комментарии, а более подробно они будут разобраны дальше.
Пример 4.
class EggVoice extends Thread
{
@Override
public void run()
{
for(int i = 0; i < 5; i++)
{
try{
sleep(1000); //Приостанавливает поток на 1 секунду
}catch(InterruptedException e){}
System.out.println("яйцо!");
}
//Слово «яйцо» сказано 5 раз
}
}
public class ChickenVoice //Класс с методом main()
{
static EggVoice mAnotherOpinion; //Побочный поток
public static void main(String[] args)
{
mAnotherOpinion = new EggVoice(); //Создание потока
System.out.println("Спор начат...");
mAnotherOpinion.start(); //Запуск потока
for(int i = 0; i < 5; i++)
{
try{
Thread.sleep(1000); //Приостанавливает поток на 1 секунду
}catch(InterruptedException e){}
System.out.println("курица!");
}
//Слово «курица» сказано 5 раз
if(mAnotherOpinion.isAlive()) //Если оппонент еще не сказал последнее слово
{
try{
mAnotherOpinion.join();//Подождать пока оппонент закончит высказываться.
}catch(InterruptedException e){}
System.out.println("Первым появилось яйцо!");
}
else //если оппонент уже закончил высказываться
{
System.out.println("Первой появилась курица!");
}
System.out.println("Спор закончен!");
}
}
Консоль:
Спор начат...
курица!
яйцо!
яйцо!
курица!
яйцо!
курица!
яйцо!
курица!
яйцо!
курица!
Первой появилась курица!
Спор закончен!
В приведенном примере два потока параллельно в течении 5 секунд выводят информацию на консоль. Точно предсказать, какой поток закончит высказываться последним, невозможно. Можно попытаться, и можно даже угадать, но есть большая вероятность того, что та же программа при следующем запуске будет иметь другого «победителя». Это происходит из-за так называемого «асинхронного выполнения кода». Асинхронность означает то, что нельзя утверждать, что какая-либо инструкция одного потока, выполнится раньше или позже инструкции другого. Или, другими словами, параллельные потоки независимы друг от друга, за исключением тех случаев, когда программист сам описывает зависимости между потоками с помощью предусмотренных для этого средств языка.
Завершение процесса и демоны
В Java процесс завершается тогда, когда завершается последний его поток. Даже если метод main() уже завершился, но еще выполняются порожденные им потоки, система будет ждать их завершения. Однако это правило не относится к особому виду потоков – демонам. Если завершился последний обычный поток процесса, и остались только потоки-демоны, то они будут принудительно завершены и выполнение процесса закончится. Чаще всего потоки-демоны используются для выполнения фоновых задач, обслуживающих процесс в течение его жизни.
Объявить поток демоном достаточно просто — нужно перед запуском потока вызвать его метод setDaemon(true);
Проверить, является ли поток демоном, можно вызвав его метод boolean isDaemon();
Завершение потоков
В Java существуют (существовали) средства для принудительного завершения потока. В частности метод Thread.stop() завершает поток незамедлительно после своего выполнения. Однако этот метод, а также Thread.suspend(), приостанавливающий поток, и Thread.resume(), продолжающий выполнение потока, были объявлены устаревшими и их использование отныне крайне нежелательно. Дело в том что поток может быть «убит» во время выполнения операции, обрыв которой на полуслове оставит некоторый объект в неправильном состоянии, что приведет к появлению трудно отлавливаемой и случайным образом возникающей ошибке.
Вместо принудительного завершения потока применяется схема, в которой каждый поток сам ответственен за своё завершение. Поток может остановиться либо тогда, когда он закончит выполнение метода run(), (main() — для главного потока) либо по сигналу из другого потока. Причем как реагировать на такой сигнал — дело, опять же, самого потока. Получив его, поток может выполнить некоторые операции и завершить выполнение, а может и вовсе его проигнорировать и продолжить выполняться. Описание реакции на сигнал завершения потока лежит на плечах программиста.
Java имеет встроенный механизм оповещения потока, который называется Interruption (прерывание, вмешательство), и скоро мы его рассмотрим, но сначала посмотрите на следующую программку:
Пример 5.
Incremenator — поток, который каждую секунду прибавляет или вычитает единицу из значения статической переменной Program.mValue. Incremenator содержит два закрытых поля – mIsIncrement и mFinish. То, какое действие выполняется, определяется булевой переменной mIsIncrement — если оно равно true, то выполняется прибавление единицы, иначе — вычитание. А завершение потока происходит, когда значение mFinish становится равно true.
class Incremenator extends Thread
{
//О ключевом слове volatile - чуть ниже
private volatile boolean mIsIncrement = true;
private volatile boolean mFinish = false;
public void changeAction() //Меняет действие на противоположное
{
mIsIncrement = !mIsIncrement;
}
public void finish() //Инициирует завершение потока
{
mFinish = true;
}
@Override
public void run()
{
do
{
if(!mFinish) //Проверка на необходимость завершения
{
if(mIsIncrement)
Program.mValue++; //Инкремент
else
Program.mValue--; //Декремент
//Вывод текущего значения переменной
System.out.print(Program.mValue + " ");
}
else
return; //Завершение потока
try{
Thread.sleep(1000); //Приостановка потока на 1 сек.
}catch(InterruptedException e){}
}
while(true);
}
}
public class Program
{
//Переменая, которой оперирует инкременатор
public static int mValue = 0;
static Incremenator mInc; //Объект побочного потока
public static void main(String[] args)
{
mInc = new Incremenator(); //Создание потока
System.out.print("Значение = ");
mInc.start(); //Запуск потока
//Троекратное изменение действия инкременатора
//с интервалом в i*2 секунд
for(int i = 1; i <= 3; i++)
{
try{
Thread.sleep(i*2*1000); //Ожидание в течении i*2 сек.
}catch(InterruptedException e){}
mInc.changeAction(); //Переключение действия
}
mInc.finish(); //Инициация завершения побочного потока
}
}
Консоль:
Значение = 1 2 1 0 -1 -2 -1 0 1 2 3 4
Взаимодействовать с потоком можно с помощью метода changeAction() (для смены вычитания на сложение и наоборот) и метода finish() (для завершения потока). В объявлении переменных mIsIncrement и mFinish было использовано ключевое слово volatile (изменчивый, не постоянный). Его необходимо использовать для переменных, которые используются разными потоками. Это связано с тем, что значение переменной, объявленной без volatile, может кэшироваться отдельно для каждого потока, и значение из этого кэша может различаться для каждого из них. Объявление переменной с ключевым словом volatile отключает для неё такое кэширование и все запросы к переменной будут направляться непосредственно в память.
В этом примере показано, каким образом можно организовать взаимодействие между потоками. Однако есть одна проблема при таком подходе к завершению потока — Incremenator проверяет значение поля mFinish раз в секунду, поэтому может пройти до секунды времени между тем, когда будет выполнен метод finish(), и фактическим завершения потока. Было бы замечательно, если бы при получении сигнала извне, метод sleep() возвращал выполнение и поток незамедлительно начинал своё завершение. Для выполнения такого сценария существует встроенное средство оповещения потока, которое называется Interruption (прерывание, вмешательство).
Класс 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 после добавления поддержки прерываний:
Пример 6.
class Incremenator extends Thread
{
private volatile boolean mIsIncrement = true;
public void changeAction()//Меняет действие на противоположное
{
mIsIncrement = !mIsIncrement;
}
@Override
public void run()
{
do
{
if(!Thread.interrupted()) //Проверка прерывания
{
if(mIsIncrement) Program.mValue++; //Инкремент
else Program.mValue--; //Декремент
//Вывод текущего значения переменной
System.out.print(Program.mValue + " ");
}
else
return; //Завершение потока
try{
Thread.sleep(1000);//Приостановка потока на 1 сек.
}catch(InterruptedException e){
return; //Завершение потока после прерывания
}
}
while(true);
}
}
class Program
{
//Переменая, которой оперирует инкременатор
public static int mValue = 0;
static Incremenator mInc; //Объект побочного потока
public static void main(String[] args)
{
mInc = new Incremenator(); //Создание потока
System.out.print("Значение = ");
mInc.start(); //Запуск потока
//Троекратное изменение действия инкременатора
//с интервалом в i*2 секунд
for(int i = 1; i <= 3; i++)
{
try{
Thread.sleep(i*2*1000); //Ожидание в течении i*2 сек.
}catch(InterruptedException e){}
mInc.changeAction(); //Переключение действия
}
mInc.interrupt(); //Прерывание побочного потока
}
}
Консоль:
Значение = 1 2 1 0 -1 -2 -1 0 1 2 3 4
Как видите, мы избавились от метода finish() и реализовали тот же механизм завершения потока с помощью встроенной системы прерываний. В этой реализации мы получили одно преимущество — метод sleep() вернет управление (сгенерирует исключение) незамедлительно после прерывания потока.
Можно заметить что методы sleep() и join() обёрнуты в конструкции try-catch. Это необходимое условие работы этих методов. Вызывающий их код должен перехватывать исключение InterruptedException, которое они бросают при прерывании во время ожидания.