Средства Java для реализации многопоточной архитектуры

 

Постановка задачи


Тема курсовой работы: «Средства Java для реализации многопоточной архитектуры».

Цель работы: Используя средства многопоточной архитектуры произвести вычисления и произвести замер скорости выполнения вычисления для разного количества потоков. Для вычисления использовать:

Функцию



Количество потоков: любое;

Количество аргументов: два (x,y);

Создать интерфейс ввода и вывода результата;

Для реализации данной задачи используем следующий план действий:

1.Проведем обзор средств и методов реализации многопоточности в языке Java/

2.Построим алгоритм программы.

.Напишем по алгоритму сам текст программы.

.На примере использования нескольких потоков проанализируем результат.

.Запишем вывод о проделанной работе.

Введение


Наиболее очевидная область применения многопоточности - это программирование интерфейсов. Многопоточность незаменима тогда, когда необходимо, чтобы графический интерфейс продолжал отзываться на действия пользователя во время выполнения некоторой обработки информации. Например, поток, отвечающий за интерфейс, может ждать завершения другого потока, загружающего файл из интернета, и в это время выводить некоторую анимацию или обновлять прогресс-бар. Кроме того он может остановить поток загружающий файл, если была нажата кнопка «отмена».

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


1. Теоретическая часть (обзор средств и методов реализации многопоточности в Java)


.1 Процессы


Процесс - это совокупность кода и данных, разделяющих общее виртуальное адресное пространство. Чаще всего одна программа состоит из одного процесса, но бывают и исключения (например, браузер Chrome создает отдельный процесс для каждой вкладки, что дает ему некоторые преимущества, вроде независимости вкладок друг от друга). Процессы изолированы друг от друга, поэтому прямой доступ к памяти чужого процесса невозможен (взаимодействие между процессами осуществляется с помощью специальных средств).

Для каждого процесса ОС создает так называемое «виртуальное адресное пространство», к которому процесс имеет прямой доступ. Это пространство принадлежит процессу, содержит только его данные и находится в полном его распоряжении. Операционная система же отвечает за то, как виртуальное пространство процесса проецируется на физическую память.

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

Рис.1


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


1.2 Потоки


Один поток - это одна единица исполнения кода. Каждый поток последовательно выполняет инструкции процесса, которому он принадлежит, параллельно с другими потоками этого процесса.

Следует отдельно обговорить фразу «параллельно с другими потоками». Известно, что на одно ядро процессора, в каждый момент времени, приходится одна единица исполнения. То есть одноядерный процессор может обрабатывать команды только последовательно, по одной за раз (в упрощенном случае). Однако запуск нескольких параллельных потоков возможен и в системах с одноядерными процессорами. В этом случае система будет периодически переключаться между потоками, поочередно давая выполняться то одному, то другому потоку. Такая схема называется псевдо-параллелизмом. Система запоминает состояние (контекст) каждого потока, перед тем как переключиться на другой поток, и восстанавливает его по возвращению к выполнению потока.

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

Проще говоря, при псевдопараллельном выполнении потоков процессор мечется между выполнением нескольких потоков, выполняя по очереди часть каждого из них (рис.2).


Рис.2


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

То, что инструкции параллельных потоков выполняются вперемешку, в некоторых случаях может привести к конфликтам доступа к данным. Проблемам взаимодействия потоков будет посвящена следующая статья, а пока о том, как запускаются потоки в Java.


1.3 Запуск потоков


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

В языке Java поток представляется в виде объекта-потомка класса Thread. Этот класс инкапсулирует стандартные механизмы работы с потоком.

Запустить новый поток можно двумя способами:

Способ 1

Создать объект класса Thread, передав ему в конструкторе нечто, реализующее интерфейс Runnable. Этот интерфейс содержит метод run(), который будет выполняться в новом потоке. Поток закончит выполнение, когда завершится его метод run().

Выглядит это так (Пример 1.):


class SomeThing//Нечто, реализующее интерфейс RunnableRunnable//(содержащее метод run())

{void run()//Этот метод будет выполняться в побочном потоке

{.out.println("Привет из побочного потока!");

}

}class Program//Класс с методом main()

{SomeThing mThing;//mThing - объект класса, реализующего интерфейс Runnablestatic void main(String[] args)

{= new SomeThing();myThready = new Thread(mThing);//Создание потока "myThready".start();//Запуск потока.out.println("Главный поток завершён...");

}

}


Для пущего укорочения кода можно передать в конструктор класса Thread объект безымянного внутреннего класса, реализующего интерфейс Runnable:

Пример 2.


public class Program//Класс с методом main().

{static void main(String[] args)

{

//Создание потокаmyThready = new Thread(new Runnable()

{void run() //Этот метод будет выполняться в побочном потоке

{.out.println("Привет из побочного потока!");

}

});.start();//Запуск потока.out.println("Главный поток завершён...");

}

}


Способ 2 (Пример3.)


Создать потомка класса Thread и переопределить его метод run():AffableThread extends Thread

{

@Overridevoid run()//Этот метод будет выполнен в побочном потоке

{.out.println("Привет из побочного потока!");

}

}class Program

{AffableThread mSecondThread;static void main(String[] args)

{= new AffableThread();//Создание потока.start();//Запуск потока

System.out.println("Главный поток завершён...");

}

}


В приведённом выше примере в методе main() создается и запускается еще один поток. Важно отметить, что после вызова метода mSecondThread.start() главный поток продолжает своё выполнение, не дожидаясь пока порожденный им поток завершится. И те инструкции, которые идут после вызова метода start(), будут выполнены параллельно с инструкциями потока mSecondThread.

Для демонстрации параллельной работы потоков рассмотрим программу, в которой два потока спорят на предмет философского вопроса «что было раньше, яйцо или курица?». Главный поток уверен, что первой была курица, о чем он и будет сообщать каждую секунду. Второй же поток раз в секунду будет опровергать своего оппонента. Всего спор продлится 5 секунд. Победит тот поток, который последним изречет свой ответ на этот, без сомнения, животрепещущий философский вопрос. В примере используются средства, (isAlive() sleep() и join()). К ним даны комментарии, а более подробно они будут разобраны дальше.

Пример 4


class EggVoice extends Thread

{

@Overridevoid run()

{(int i = 0; i < 5; i++)

{

try{

sleep(1000);//Приостанавливает поток на 1 секунду

}catch(InterruptedException e){}

.out.println("яйцо!");

}

//Слово «яйцо» сказано 5 раз

}

}

class ChickenVoice//Класс с методом main()

{EggVoice mAnotherOpinion;//Побочный потокstatic void main(String[] args)

{= new EggVoice();//Создание потока.out.println("Спор начат...");.start(); //Запуск потока(int i = 0; i < 5; i++)

{{.sleep(1000);//Приостанавливает поток на 1 секунду

}catch(InterruptedException e){}.out.println("курица!");

}


//Слово «курица» сказано 5 раз(mAnotherOpinion.isAlive()) //Если оппонент еще не сказал последнее слово

{{.join();//Подождать пока оппонент закончит высказываться.

}catch(InterruptedException e){}.out.println("Первым появилось яйцо!");

}//если оппонент уже закончил высказываться

{.out.println("Первой появилась курица!");

}.out.println("Спор закончен!");

}

}

Консоль:

Спор начат...

курица!

яйцо!

яйцо!

курица!

яйцо!

курица!

яйцо!

курица!

яйцо!

курица!

Первой появилась курица!

Спор закончен!


В приведенном примере два потока параллельно в течении 5 секунд выводят информацию на консоль. Точно предсказать, какой поток закончит высказываться последним, невозможно. Можно попытаться, и можно даже угадать, но есть большая вероятность того, что та же программа при следующем запуске будет иметь другого «победителя». Это происходит из-за так называемого «асинхронного выполнения кода». Асинхронность означает то, что нельзя утверждать, что какая-либо инструкция одного потока, выполнится раньше или позже инструкции другого. Или, другими словами, параллельные потоки независимы друг от друга, за исключением тех случаев, когда программист сам описывает зависимости между потоками с помощью предусмотренных для этого средств языка.


1.4 Завершение процесса и демоны


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

Объявить поток демоном достаточно просто - нужно перед запуском потока вызвать его метод setDaemon(true);

Проверить, является ли поток демоном, можно вызвав его метод boolean isDaemon();


1.5 Завершение потоков


В Java существуют (существовали) средства для принудительного завершения потока. В частности метод Thread.stop() завершает поток незамедлительно после своего выполнения. Однако этот метод, а также Thread.suspend(), приостанавливающий поток, и Thread.resume(), продолжающий выполнение потока, были объявлены устаревшими и их использование отныне крайне нежелательно. Дело в том что поток может быть «убит» во время выполнения операции, обрыв которой на полуслове оставит некоторый объект в неправильном состоянии, что приведет к появлению трудно отлавливаемой и случайным образом возникающей ошибке.

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

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

Пример 5.- поток, который каждую секунду прибавляет или вычитает единицу из значения статической переменной Program.mValue. Incremenator содержит два закрытых поля - mIsIncrement и mFinish. То, какое действие выполняется, определяется булевой переменной mIsIncrement - если оно равно true, то выполняется прибавление единицы, иначе - вычитание. А завершение потока происходит, когда значение mFinish становится равно true.


class Incremenator extends Thread

{

//О ключевом слове volatile - чуть нижеvolatile boolean mIsIncrement = true;volatile boolean mFinish = false;


public void changeAction()//Меняет действие на противоположное

{= !mIsIncrement;

}void finish()//Инициирует завершение потока

{= true;

}


@Overridevoid run()

{

{(!mFinish)//Проверка на необходимость завершения

{(mIsIncrement).mValue++;//Инкремент

Program.mValue--;//Декремент


//Вывод текущего значения переменной

System.out.print(Program.mValue + " ");

};//Завершение потока

{.sleep(1000);//Приостановка потока на 1 сек.

}catch(InterruptedException e){}

}(true);

}

}

class Program

{

//Переменая, которой оперирует инкременатор

public static int mValue = 0;

Incremenator mInc;//Объект побочного потока

static void main(String[] args)

{= new Incremenator();//Создание потока

.out.print("Значение = ");


mInc.start();//Запуск потока


//Троекратное изменение действия инкременатора

//с интервалом в i*2 секунд(int i = 1; i <= 3; i++)

{{.sleep(i*2*1000); //Ожидание в течении i*2 сек.

}catch(InterruptedException e){}

.changeAction();//Переключение действия

}.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

{volatile boolean mIsIncrement = true;


public void changeAction()//Меняет действие на противоположное

{= !mIsIncrement;

}


@Overridevoid run()

{

{(!Thread.interrupted())//Проверка прерывания

{(mIsIncrement) Program.mValue++;//ИнкрементProgram.mValue--;//Декремент


//Вывод текущего значения переменной.out.print(Program.mValue + " ");

};//Завершение потока

{.sleep(1000);//Приостановка потока на 1 сек.

}catch(InterruptedException e){;//Завершение потока после прерывания

}

}(true);

}

}

Program

{

//Переменая, которой оперирует инкременатор

public static int mValue = 0;

Incremenator mInc;//Объект побочного потокаstatic void main(String[] args)

{= new Incremenator();//Создание потока.out.print("Значение = ");


mInc.start();//Запуск потока


//Троекратное изменение действия инкременатора

//с интервалом в i*2 секунд(int i = 1; i <= 3; i++)

{{.sleep(i*2*1000);//Ожидание в течении i*2 сек.

}catch(InterruptedException e){}

.changeAction();//Переключение действия

}.interrupt();//Прерывание побочного потока

}

}


Консоль:

Значение = 1 2 1 0 -1 -2 -1 0 1 2 3 4


Как видите, мы избавились от метода finish() и реализовали тот же механизм завершения потока с помощью встроенной системы прерываний. В этой реализации мы получили одно преимущество - метод sleep() вернет управление (сгенерирует исключение) незамедлительно после прерывания потока. Можно заметить что методы sleep() и join() обёрнуты в конструкции try-catch. Это необходимое условие работы этих методов. Вызывающий их код должен перехватывать исключение InterruptedException, которое они бросают при прерывании во время ожидания.

2. Методы использующиеся при работе с потоками


.1. Метод Thread.sleep()

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

Метод sleep() может выполняться либо заданное кол-во времени (миллисекунды или наносекунды) либо до тех пор пока он не будет остановлен прерыванием (в этом случае он сгенерирует исключение InterruptedException).

Пример 7.

.sleep(1500); //Ждет полторы секунды.sleep(2000, 100); //Ждет 2 секунды и 100 наносекунд


Несмотря на то, что метод sleep() может принимать в качестве времени ожидания наносекунды, не стоит принимать это всерьез. Во многих системах время ожидания все равно округляется до миллисекунд а то и до их десятков.


2.2. Метод yield()


Статический метод Thread.yield() заставляет процессор переключиться на обработку других потоков системы.

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

В этом случае можно поместить проверку события и метод Thread.yield() в цикл:

Пример 8.


//Ожидание поступления сообщения(!msgQueue.hasMessages())//Пока в очереди нет сообщений

{.yield();//Передать управление другим потокам

}


2.3. Метод join()


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

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

С помощью задания времени ожидания потока можно, например, выполнять обновление анимированной картинки пока главный (или любой другой) поток ждёт завершения побочного потока, выполняющего ресурсоёмкие операции:

Пример 9.


Thinker brain = new Thinker(); //Thinker - потомок класса Thread..start();//Начать "обдумывание".

{.refresh();//mThinkIndicator - анимированная картинка.


try{.join(250);//Подождать окончания мысли четверть секунды.

}catch(InterruptedException e){}

}(brain.isAlive());//Пока brain думает...


//brain закончил думать (звучат овации).


В этом примере поток brain (мозг) думает над чем-то, и предполагается, что это занимает у него длительное время. Главный поток ждет его четверть секунды и, в случае, если этого времени на раздумье не хватило, обновляет «индикатор раздумий» (некоторая анимированная картинка). В итоге, во время раздумий, пользователь наблюдает на экране индикатор мыслительного процесса, что дает ему знать, что электронные мозги чем то заняты.


3. Приоритеты потоков


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

Работать с приоритетами потока можно с помощью двух функций:setPriority(int priority) - устанавливает приоритет потока.

Возможные значения priority - MIN_PRIORITY, NORM_PRIORITY и MAX_PRIORITY.int getPriority() - получает приоритет потока.

Некоторые полезные методы класса Thread работы с потоками.isAlive() - возвращает true если myThready() выполняется и false если поток еще не был запущен или был завершен.

setName(String threadName) - Задает имя потока.

String getName() - Получает имя потока.

Имя потока - ассоциированная с ним строка, которая в некоторых случаях помогает понять, какой поток выполняет некоторое действие. Иногда это бывает полезным.Thread Thread.currentThread() - статический метод, возвращающий объект потока, в котором он был вызван.getId()- возвращает идентификатор потока. Идентификатор - уникальное число, присвоенное потоку.

блокировка голодание поток запуск

4. Проблемы при реализации параллельных программ


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


4.1 Взаимная блокировка (deadlock)


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

Пример 10.


public class DeadLock {static void main(String[] args) {ReentrantLock lock1 = new ReentrantLock();ReentrantLock lock2 = new ReentrantLock();t = new Thread(new Runnable() {

@Overridevoid run() {.lock();();.lock();{

//критическая секция

}{.unlock();.unlock();

}

}

}, "slave");.start();.lock();();

lock1.lock();{

//критическая секция

}

finally {.unlock();.unlock();

}.out.println("finished");

}

/**

* Задержка на 1 секунду

*/static void delay() {{.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}


Выполнение кода представленного выше никогда не приведёт к выводу на экран строки "finished". Потоки main и slave будут заблокированы навсегда после того как первый захватит lock1, а второй lock2. В таком маленьком фрагменте кода не вызовет проблем детектировать взаимную блокировку просто посмотрев листинг. В реальности не всегда получается это сделать на этапе разработки (хотя возможно использовать статический анализатор кода), поэтому требуются методы детектирования таких ситуаций во время выполнения. Наиболее удобным методом является изучения дампа потоков, который можно получить с помощью утилиты jstack, либо послав сигнал выполняемому процессу (для консольного windows приложения Ctrl+Break, в linux сигнал SIGQUIT). Для нашего примера часть дампа потоков будет выглядеть подобно представленному ниже.

Пример 11.

one Java-level deadlock:

=============================

"slave":for ownable synchronizer 0x2299c870,

(a java.util.concurrent.locks.ReentrantLock$NonfairSync),is held by "main"

"main":for ownable synchronizer 0x2299c848,

(a java.util.concurrent.locks.ReentrantLock$NonfairSync),is held by "slave"stack information for the threads listed above:

===================================================

"slave":sun.misc.Unsafe.park(Native Method)

parking to wait for <0x2299c870>

(a java.util.concurrent.locks.ReentrantLock$NonfairSync)java.util.concurrent.locks.LockSupport.park(Unknown Source)

... опущена часть вывода несущественная в контексте обсуждения

at java.lang.Thread.run(Unknown Source)

"main":sun.misc.Unsafe.park(Native Method)

parking to wait for <0x2299c848>

(a java.util.concurrent.locks.ReentrantLock$NonfairSync)java.util.concurrent.locks.LockSupport.park(Unknown Source)

... опущена часть вывода несущественная в контексте обсуждения

at kz.pnhz.test.sandbox.DeadLock.main(DeadLock.java:31)


Found 1 deadlock.

... опущена часть вывода несущественная в контексте обсуждения


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


4.2. Голодание (starvation)


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

Пример 12.


public class Starvation {static void main(String[] args) {ReentrantLock lock1 = new ReentrantLock();t = new Thread(new Runnable() {

@Overridevoid run() {();//реализация аналогична DeadLock.lock();{

//критическая секция.out.println("completed");

}{.unlock();

}

}

}, "slave");.start();.lock();{(!Thread.currentThread().isInterrupted()) {

//условие выхода которое не наступает

}

}{.unlock();

}.out.println("finished");

}

}


Детектирование проблемы голодания не столь прямолинейно как в случае с взаимной блокировкой. Если ситуация подобна примеру, то можно легко вычислить не ожидающий поток с бесконечным циклом любой утилитой отображающей состояние процессов и потоков в операционной системе. Далее получить дамп потоков и определить место возникновения проблемы. Но если в такой ситуации внутри цикла поток будет блокироваться в ожидании события или просто отдавать рекомендацию планировщику (Thread.yield, Thread.sleep) уступить текущий квант, то определить её будет сложнее. При подобных условиях могут пригодиться как ручное наблюдение за стеками выполнения и состояниями потоков во времени, так и автоматическое профилирование приложения (может понадобиться длительное время сбора статистики).


4.3 Активная блокировка (livelock)


Это зацикливание алгоритма в результате взаимодействия состоянием, которое в свою очередь может независимо изменяться извне. Условие существование изменяемого извне состояния - параллелизм (необязательно многопоточность, например, взаимодействие процессов с файловой системой). При активной блокировки поток не блокируется, а продолжает попытки выполнить полезное действие, но в результате некорректной обработки ошибки при его выполнение повторяет его снова. Например завершение транзакции в СУБД, которая не может быть выполнена по причине нарушения в работе сервера, но некорректный код трактует ошибку как временную и пытается повторить транзакцию. Ещё один классический пример из области сетевых технологий. Если два или более передающих устройства одновременно пытаются передать данные, то обнаруживая коллизию и повторяя попытки передачи через равные интервалы времени ни одно устройство не сможет ничего передать, так как каждый раз разделяемая среда будет передавать сигналы нескольких источников.

Помимо таких явных проблем блокирующих выполнение, как упомянутые выше в этой секции, существуют и менее критичные проблемы производительности и латентности (времени реакции). Наверное, наиболее распространённым их видом, связанным с производительностью, будет являться проблема со слабо гранулированными блокировками (coarse-grained lock) или их экстремальным вариантом - глобальными блокировками (global lock). Суть этой проблемы сводится к тому, что в результате больших размеров критической секции выполнение на этом участке фактически сериализуется (смотри 2.4 Блокировки). К проблемам латентности можно отнести ситуацию близкую к голоданию, когда какой-то поток практически не получает время центрального процессора или прочий ресурс, в результате чего растягивается время выполнения операции.

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

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

Алгоритм работы программы

Согласно этому алгоритму напишем текст программы. Листинг программы приведем ниже.


Приложение 1


import javax. swing.*; //подключение библиотеки вывода окна //на экран


public class Vine {


static int x=0,y=0,z=1; //инициализация общих аргументов //функции

//**********************************подсчет c использованием многопоточностиstatic class ThreadTest implements Runnable { before1=0;long after1=0; //переменные подсчета времени

public void run() {

double calc; //переменная подсчета значений функ//ции

before1 = System.currentTimeMillis(); //установка значений переменной вре//мени «до»

for (int i=0; i<5000; i++) {

//подсчет значений функции=(Math.sin(i*y)*Math.cos(z*2))+(Math.cos(i*y)*Math.sin(2*z));

//условие вывода на экран промежу//точного значения

if (i%1000==0) {//условие вывода промеж. знач

after1=System.currentTimeMillis(); //установка значений переменной вре //мени «после»

//вывод на экран промежуточного //значения

System.out.format("%s count %d fixing %.4f time %d ms\n",getName(),(i/1000),calc,(after1-before1));


}

}

}

}static String getName() {Thread.currentThread().getName(); //возвращение имени потока

}

//*********************************подсчет без использования многопоточностиstatic void free() {

long before2=0;long after2=0; //переменные подсчета времени

before2= System.currentTimeMillis();//установка значений переменной вре//мени «до»

double calc; //переменная подсчета значений функ//ции

for ( int i=0; i<5000; i++) {

//подсчет значений функции=(Math.sin(i*y)*Math.cos(z*2))+(Math.cos(i*y)*Math.sin(2*z)); (i%1000==0) {//условие вывода промеж. знач

after2=System.currentTimeMillis();//установка значений переменной вре //мени «после»

//вывод на экран промежуточного //значения

System.out.format("count %d fixing: %.4f time %d ms\n",(i/1000),calc,(after2-before2));

}

}

}

static void main(String[] args) {//функция main()

//вывод запроса на экран

String input_x = JOptionPane.showInputDialog("Введите количество потоков:");

x = Integer.parseInt(input_x);//считывание значения в переменную

//вывод запроса на экран

String input_y = JOptionPane.showInputDialog ("Введите аргумент A:") ;

y = Integer.parseInt(input_y); //считывание значения в переменную

//вывод запроса на экранinput_z = JOptionPane.showInputDialog ("Введите аргумент B:") ;

z = Integer.parseInt(input_z); //считывание значения в переменную

//вывод на консоль текста.out.format("<Выполнение программы без использования многопоточности>\n");();//вызов процедуры подсчета значения //функции без многопоточности

//вывод на консоль текста.out.format("<Выполнение программы c использованием многопоточности>\n");

// Подготовка потоковt[] = new Thread[x];(int i=0; i<t.length; i++) {[i]=new Thread(new ThreadTest(),"Thread "+i);

}

// Запуск потоков(int i=0; i<t.length; i++) {[i].start();.out.format("%s <starter> \n",t[i].getName());

}. exit(0); //завершение работы программы

}


}


Проанализируем результат выполнения программы. Результат выполнения программы приведен ниже.


Приложение 2


Результат 1.


При работе 5 потоков.

<Выполнение программы без использования многопоточности>

count 0 fixing: 0,9093 time 0 ms1 fixing: 0,9974 time 16 ms2 fixing: 0,5394 time 16 ms3 fixing: -0,9384 time 16 ms 4 fixing: 0,5476 time 31 ms

<Выполнение программы c использованием многопоточности>

Thread 0 <starter>1 <starter>2 <starter>2 count 0 fixing 0,9093 time 0 ms2 count 1 fixing 0,9974 time 47 ms2 count 2 fixing 0,5394 time 47 ms2 count 3 fixing -0,9384 time 47 ms1 count 0 fixing 0,9093 time 0 ms1 count 1 fixing 0,9974 time 62 ms1 count 2 fixing 0,5394 time 62 ms1 count 3 fixing -0,9384 time 62 ms1 count 4 fixing 0,5476 time 78 ms3 <starter>4 <starter>0 count 0 fixing 0,9093 time 0 ms0 count 1 fixing 0,9974 time 78 ms0 count 2 fixing 0,5394 time 78 ms0 count 3 fixing -0,9384 time 94 ms0 count 4 fixing 0,5476 time 94 ms2 count 4 fixing 0,5476 time 109 ms3 count 0 fixing 0,9093 time 0 ms3 count 1 fixing 0,9974 time 0 ms3 count 2 fixing 0,5394 time 16 ms3 count 3 fixing -0,9384 time 16 ms3 count 4 fixing 0,5476 time 16 ms4 count 0 fixing 0,9093 time 0 ms4 count 1 fixing 0,9974 time 0 ms4 count 2 fixing 0,5394 time 0 ms4 count 3 fixing -0,9384 time 16 ms 4 count 4 fixing 0,5476 time 16 ms


Анализ: Согласно результату 1. видно что выполнении вычисления происходит за 31 мс, однако при использовании разбиения на 5 потоков мы видим что 0,1 и 2 поток слишком сильно запаздывают, а вот 3 и 4 поток выполняют вычисления за 16 мс. Такое запаздывание я связываю с загруженностью процессора другими процессами.

Для объективности проведем еще пару тестов с другим количеством потоков.

Результат 2.

При работе 3 потоков.


<Выполнение программы без использования многопоточности>

count 0 fixing: 0,9093 time 0 ms1 fixing: 0,9974 time 15 ms2 fixing: 0,5394 time 62 ms3 fixing: -0,9384 time 62 ms 4 fixing: 0,5476 time 62 ms

<Выполнение программы c использованием многопоточности>

Thread 0 <starter>1 <starter>0 count 0 fixing 0,9093 time 0 ms0 count 1 fixing 0,9974 time 0 ms0 count 2 fixing 0,5394 time 16 ms0 count 3 fixing -0,9384 time 16 ms0 count 4 fixing 0,5476 time 16 ms1 count 0 fixing 0,9093 time 0 ms1 count 1 fixing 0,9974 time 0 ms1 count 2 fixing 0,5394 time 0 ms1 count 3 fixing -0,9384 time 0 ms1 count 4 fixing 0,5476 time 16 ms2 <starter>2 count 0 fixing 0,9093 time 0 ms2 count 1 fixing 0,9974 time 15 ms2 count 2 fixing 0,5394 time 15 ms2 count 3 fixing -0,9384 time 15 ms

Thread 2 count 4 fixing 0,5476 time 15 ms


Согласно результату 2. видно что выполнении вычисления происходит за 62 мс, если рассматривать этот результат в связке с предыдущим результатом, то видно что, одно и то же вычисление, происходит за разное время. Данный результат подтверждает то что на выполнение вычисления, влияют другие сторонние процессы, а так же использование не совершенного метода подсчета времени. Не зависимо от не совершенства метода из этого результата видно что при использовании разбиения на 3 потока значительное увеличение скорости вычисления, все потоки выполняют задачу за 16 мс.

Результат 3.


При работе 10 потоков.

<Выполнение программы без использования многопоточности>

count 0 fixing: 0,9093 time 0 ms1 fixing: 0,9974 time 16 ms2 fixing: 0,5394 time 16 ms3 fixing: -0,9384 time 31 ms 4 fixing: 0,5476 time 31 ms

<Выполнение программы c использованием многопоточности>

Thread 0 <starter>1 <starter>0 count 0 fixing 0,9093 time 0 ms0 count 1 fixing 0,9974 time 0 ms0 count 2 fixing 0,5394 time 0 ms0 count 3 fixing -0,9384 time 0 ms0 count 4 fixing 0,5476 time 16 ms1 count 0 fixing 0,9093 time 0 ms1 count 1 fixing 0,9974 time 0 ms1 count 2 fixing 0,5394 time 15 ms1 count 3 fixing -0,9384 time 15 ms1 count 4 fixing 0,5476 time 15 ms2 <starter>3 <starter>4 <starter>5 <starter>6 <starter>7 <starter>2 count 0 fixing 0,9093 time 0 ms2 count 1 fixing 0,9974 time 0 ms2 count 2 fixing 0,5394 time 0 ms2 count 3 fixing -0,9384 time 16 ms2 count 4 fixing 0,5476 time 16 ms3 count 0 fixing 0,9093 time 0 ms3 count 1 fixing 0,9974 time 0 ms3 count 2 fixing 0,5394 time 15 ms3 count 3 fixing -0,9384 time 15 ms3 count 4 fixing 0,5476 time 15 ms4 count 0 fixing 0,9093 time 0 ms4 count 1 fixing 0,9974 time 16 ms4 count 2 fixing 0,5394 time 16 ms4 count 3 fixing -0,9384 time 16 ms8 <starter>9 <starter>7 count 0 fixing 0,9093 time 0 ms7 count 1 fixing 0,9974 time 0 ms7 count 2 fixing 0,5394 time 0 ms7 count 3 fixing -0,9384 time 0 ms7 count 4 fixing 0,5476 time 16 ms6 count 0 fixing 0,9093 time 0 ms6 count 1 fixing 0,9974 time 16 ms6 count 2 fixing 0,5394 time 16 ms6 count 3 fixing -0,9384 time 32 ms6 count 4 fixing 0,5476 time 32 ms4 count 4 fixing 0,5476 time 63 ms8 count 0 fixing 0,9093 time 0 ms8 count 1 fixing 0,9974 time 0 ms8 count 2 fixing 0,5394 time 0 ms8 count 3 fixing -0,9384 time 16 ms8 count 4 fixing 0,5476 time 16 ms5 count 0 fixing 0,9093 time 0 ms5 count 1 fixing 0,9974 time 63 ms5 count 2 fixing 0,5394 time 79 ms5 count 3 fixing -0,9384 time 79 ms5 count 4 fixing 0,5476 time 79 ms9 count 0 fixing 0,9093 time 0 ms9 count 1 fixing 0,9974 time 0 ms9 count 2 fixing 0,5394 time 0 ms9 count 3 fixing -0,9384 time 0 ms

Thread 9 count 4 fixing 0,5476 time 16 ms


Анализ: Согласно результату 3. видно, что выполнении вычисления происходит за 31 мс, однако при использовании разбиения на 10 потоков мы видим что 4,5 и 6 поток слишком сильно запаздывают, а вот остальные потоки выполняют вычисления за 16 мс что говорит об оптимальным временем выполнения вычисления.

Если посмотреть на результаты анализов тестирования то можно сделать следующие выводы:

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

2.Необходимо использовать большее количество тестов для большей достоверности результата, с использованием других функций и большего количества аргументов.

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


Таблица 1

ФакторОписаниеКак обнаружить/ определить проблему?Что было сделано для ее устранения?Издержки запуска библиотеки с поддержкой потоковОднократная издержка при запуске кода. Не имеет значения для большей части кода. Компонуется с запуском остальной части приложенияЗа пределами типовых измерений внутри кода сравнить время выполнения последовательного приложения с многопоточнымНе применимо Не подлежит настройке разработчикомИздержки запуска потоковВремя создания потоков. Однократные затраты связанные с группировкой потоков.Измерить несколько проходов во всем многопоточном коде;Для компенсации этой издержки по возможности распределить по потокам циклы более высокого уровня или более длительные циклыИздержки на каждый поток (планирование цикла)Время, затраченное библиотекой с поддержкой многопоточности на распределение блоков задач по потокамПровести измерения только в коде, зависящем от быстродействия, или нетипичном планировщике задач; см. нижеНастроить планирование в своем коде;Издержки управления блокировкойВремя, потраченное на управление блокировкой в важных разделах. Иногда используется при эталонном тестировании и сравнении разных реализаций.Понаблюдать за частыми вызовами блокировки с помощью такого инструмента, как VTune Performance Analyzer. В большинстве случаев в коде, отягощенном блокировкой, возникают гораздо более серьезные проблемы с блокировкой;Сократите блокировку для сокращения конфликтов блокировки и управления;

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


Заключение


Объектом исследования в курсовой работе является Средства Java для реализации многопоточной архитектуры.

Целью работы является: Используя средства многопоточной архитектуры произвести вычисления и произвести замер скорости выполнения вычисления для разного количества потоков

Данная программа реализованная в среде java eclipse с использованием класса java.lang.Thread, интерфейса Runnable, содержащий метод run(), демонстрирует возможности реализации многопоточной архитектуры.

При выполнении работы выполнено следующее:

1.Проведен обзор средств и методов реализации многопоточности в языке Java.

2.Построен алгоритм программы.

.Согласно алгоритму написан и откомпилирован текст.

.На примере использования нескольких потоков проанализирован результат.

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


Постановка задачи Тема курсовой работы: «Средства Java для реализации многопоточной архитектуры». Цель работы: Используя средства многопоточ

Больше работ по теме:

КОНТАКТНЫЙ EMAIL: [email protected]

Скачать реферат © 2017 | Пользовательское соглашение

Скачать      Реферат

ПРОФЕССИОНАЛЬНАЯ ПОМОЩЬ СТУДЕНТАМ