Лекция Mногонишково програмиране в java понятието нишка (thread) се свързва с контрола или изпълнението на отделна програма или задача. Нишките са опънати във времето и свързват една с друга последователните команди



Дата21.01.2017
Размер72.98 Kb.
#13225
ТипЛекция


Лекция 9. Mногонишково програмиране

В Java понятието нишка (thread) се свързва с контрола или изпълнението на отделна програма или задача. Нишките са опънати във времето и свързват една с друга последователните команди. При многонишковото програмиране имаме паралелно изпълнение на множество задачи т.е. имаме паралелно или многозадачно програмиране. Очевидно трябва да имаме поне една нишка. Когато виртуалната джава машина стартира главния метод се създава една нишка докато се изпълни методът. Главната нишка обаче може да създаде нови нишки, чиито изпълнение може да продължи и след като главната програма е приключила. В графичните програми, наред с главната нишка, има поне една допълнителна нишка, която отговаря за следенето на събитията и за рисуване на графичните компоненти на екрана. Тази втора нишка се създава в момента, в който се отвори прозорец. Така че вече сме практикували паралелно програмиране при графичните приложения. Разбира се паралелното програмиране може да има много по-интересни приложения.



Създаване и стартиране на нишки

В Java нишката се представя от обект на вградения клас Thread или от обект на негов подклас. Целта на обекта Thread е да стартира един метод, който се изпълнява под контрола и в рамките на нишката, възможно паралелно с други нишки (методи). Когато методът завърши не е възможно нишката да се стартира отново от същия обект.

Едини начин да се създаде нишка е да се създаде подклас на Thread и да се стартира в този подклас метод public void run( ). Това е метода, който определя задачата която трябва да се изпълни от нишката т.е. когато се стартира нишката се изпълнява метода run( ). В следния пример имаме прост и доста ненужен клас, който обаче дефинира нишка, която не прави нищо друго, но извежда съобщение на екрана.



Първо, разбира се, трябва да създадем обект на класа, например:

NamedThread greetings = new NamedThread(“Fred”);
Създавайки обекта не означава автоматично стартиране на нишката. За това е необходимо обекта да извика метода start().

greetings.start();

Целта на метода start() е да създаде нишка, която ще стартира метода run( ), принадлежащ на вградения клас Thread. Новата нишка върви успоредно с нишката, в която беше извикан метода start() и заедно с всички други нишки, които вече съществуват. Това означава, че кодът в метода run() ще се изпълни в същото време както командите, които следват след извикването на greetings.start() . Нека да разгледаме следния код:

След като се изпълни командата greetings.start() имаме две нишки. Едната ще отпечата “Thread has been started.” Докато другата нишка иска да отпечата “Greetings from thread ‘Fred’!”. Двете нишки вървят едновременно и ще се борят за достъп до екрана, за да могат да отпечатат съобщенията си. Която нишка първа получи достъп, първа ще отпечата съобщението си. В една нормална еднонишкова програма нещата се извършват в един определен предсказуем ред. В многонишковата програма обаче има неопределеност. Не можем да знаем в какъв ред ще излязат съобщенията. Тази неопределеност е това нещо, което прави паралелното програмиране толкова трудно.

Трябва да отбележим че командата greetings.start() е много различна от командата greetings.run(). Втората команда ще стартира метода run( ) в същата нишка, без да създава нова нишка. Това означава, че цялата работа в метода run( ) ще бъде извършена преди компютърърт да изпълни командите след greetings.run() т.е. няма паралелизъм нито неопределеност.

Има два начина за програмиране на нишка. Първият начин, който разгледахме по-горе, е чрез дефиниране на подклас на вградения клас Thread. Вторият начин е чрез дефиниране на клас, който реализира вградения интърфейс Runnable. Този интърфейс има единствен задължителен метод public void run(), който трябва да се реализира.





За да използваме тази версия трябва първо да създадем обект NamedRunnable и след това подаваме този обект като параметър на конструктора на класа Thread

за да създадем обект на Thread.

Накрая, може да се дефинира нишка с помощта на анонимен вътрешен клас както следва:



Когато има повече нишки за изпълнение отколкото са процесорите в компютъра, компютърът разделя вниманието си между всички течащи нишки като превключва бързо от една нишка на друга. Така всеки процесор подкарва една нишка за известно време, след това подкарва друга нишка за малко и т.н. Обикновено превключванията са над 100 пъти в секунда. В резултат компютърът напредва по всички задачи, а за потребителя изглежда сякаш всички задачи се изпълняват едновременно.



Действия върху нишки

Класът Thread съдържа няколко полезни метода, освен start(), като ще споменем някои от тях.

Ако thrd е обект на Thread тогава булевият метод thrd.isAlive() показва дали нишката и жива. Счита се че нишката е жива от момента, в който стартира до момента в който приключва.

Статичният метод Thread.sleep(milliseconds) “приспива” нишката, която го вика, за указаното време в милисекунди. Спящата нишка е жива, но е в период на пауза. По този начин се въвежда контролирана пауза в изпълнението на нишката.

Понякога е необходимо една нишка да чака докато завърши друга нишка. Това става с метода join() от класа Thread. Нека thrd е нишка. Тогава ако друга нишка повика thrd.join(), викащата нишка заспива докато приключи нишката thrd.

Взаимно изключване с помощта на “synchronized”

Програмирането на няколко нишки, които изпълняват независими задачи е лесно. Истинската трудност възниква когато нишките трябва да взаимодействат по някакъв начин. Един начин за взаимодействие е при споделяне на общи ресурси. Когато две нишки искат достъп до един и същи ресурс, напримир променлива или прозорец, или екран и т.н., трябва да се внимава да се избегне едновременното ползване на ресурса от повече от една нишка. Нека разгледаме простата команда:

count = count +1;

, която се състои от три операции:


  1. Вземи стойноста на променливата count.

  2. Прибави единица към стойноста

  3. Запиши новата стойност в count.

Нека да предположим, че няколко нишки изпълняват тези три стъпки. Нека докато едната нишка е между стъпка 2 и стъпка 3, друга нишка започва да изпълнява същата последователност от стъпки. Понеже първата нишка още не е записала новата стойност на count, втората нишка прочита от паметта старата стойност на count и добавя единица. След като двете нишки са изпълнили стъпка 3 стойноста на count е нараснала с 1 вместо с правилното 2! Този проблем се нарича проблем на състезанието. Той се случва, най-общо, когато една нишка е започнала някаква операция с много стъпки и се състезава да я завърше преди да бъде прекъсната . В същото време друга нишка променя стойноста на променлива или условие, от които зависи първата нишка. Друг пример за проблем на състезанието може да възникне с условието if:

if (A!=0)

B = C/A;

Ако променливата А се споделя от няколко нишки и ако няма защита срещу проблема на състезанието, е възможно докато първата нишка е проверила условието А ! = 0, но още не е изпълнила делението, втора нишка да промени стойноста на А на нула. Това означава че първата нишка ще раздели на нула, въпреки че е извършила проверката, че А е различно от нула.

За да се реши проблема на състезанието е необходимо дадена нишка до може да придобие изключителни права на достъп (exclusive access) до общия ресурс. Това се прави с помощта на синхронизирани методи (synchronized methods) и синхронизирани команди (synchronized statement). Те се използват за да защитят общите ресурси като гарантират, че само една нишка, в определен момент ще има достъп до ресурса.

Ще разгледами прост пример като избегнем състезателния проблем когато няколко нишки едновременно искат да добавят 1 към променливата counter. Това може да стане като дефинираме клас, представляващ променливата и като използваме синхронизирани методи в класа.


Ако tsc е обект на класа ThreadSafeCounter тогава всяко нишка може да повика tsc.increment() за да добави 1 към counter по абсолютно сигурен начин. Фактът че tsc.increment() е синхронизиран означава че само една нишка може да бъде в този метод в даден момент. След като дадена нишка започва да изпълнява този метод, е гарантирано че тя ще завърши метода без да може друга нишка да промени стойноста на tsc.count. Отбележете че гаранцията зависи от факта че count е частна (private) променлива. Затова всеки достъп до tsc.count става в синхронизираните методи на класа. Ако count беше public щеше да е възможно за дадена нишка да преодолее синхронизацията чрез командата, примерно tsc.count++, което може да смени стойноста на count докато друга нишка е в метода tsc.increment(). Синхронизацията не гарантира изключителен достъп, тя само гарантира взаимноизключване между всички нишки, които са синхронизирани.

Класът ThreadSafeCounter не може да предотврати всички случай на възникване на състезателния проблем. Нека да разгледаме следното условие:

където методът doSomething() изисква стойноста на count да бъде нула. Може да възникне състезателен проблем, ако втора нишка промени стойноста на count докато първата нишка се намира между двете действия. Можем да решим проблема като поставим условието в синхронизирана команда:



Забележете че синхронизираната команда взема обекта tsc като параметър. Взаимното изключване винаги е свързано с обект. Казваме че синхронизацията е върху този обект.

Основно правило в Java е че две нишки не могат да бъдат синхронизирани едновременно върху един и същ обект. Така те не могат едновременно да изпълняват код, който е синхронизиран върху този обект. Ако една нишка е синхронизирана върху даден обект и втора нишка се опитва да синхронизира върху същия обект, втората нишка е принудена да чака докато първата нишка приключи с обекта. Това се реализира с нещо, което е наречено ключ (lock). Ако една нишка иска да влезе в синхронизиран код, нишката трябва да получи ключа асоцииран към обекта. Тя освобождава ключа след като приключи изпълнението на синхронизирания код. Ако нишка А се опитва да получи ключ, който се държи от нишка В, тогава нишката А трябва да чака докато нишка В освободи ключа. Фактически нишката А ще заспи и няма да бъде събудена докато не се освободи желания ключ.

Синхронизацията решава състезателния проблем, но тя създава възможноста за възникване на друг проблем, така наречения блокиран ключ (deadlock). Това става когато една нишка чака безкрайно за ресурс, който не може да получи.

Чакане и уведомяване (wait() и notify())

Нишките могат да комуникират и по други начини освен чрез общи ресурси. Например една нишка може да генерира резултат, който да се използват от друга нишка. Това налага някои ограничения върху реда, в който нишките извършват свойте изчисления. Ако втората нишка стигне точката, в която се нуждае от резултатите на първата нишка, тя трябва да спре и да чака да се получат тези резултати. Тъй като втората нишка не може да продължи, тя заспива. Но тогава трябва да има начин да се уведоми втората нишка когато резултата е готов, така че тя да се събуди и да продължи изчисленията. За тази цел Java разполага с методите wait() и notify(), които принадлежат на класа Object и така могат да се използват от всеки обект. Идеята е че когато една нишка вика метода wait() в някой обект, тази нишка заспива докато не се извика метода notify() в същия обект. Например нека нишка А вика wait() когато тя се нуждае от резултат от нишка В, но резултатът още не е готов. Когато нишка В е готова с резултата тя вика метода notify(), който ще събуди нишка А, така че да използва резултата. За да реализира това нишката А ще стартира следния код, където obj е някакъв обект.



докато нишка В ще извърши нещо, например както следва:



За да се избегне състезателния проблем кодът за нишката А и нишката В трябва да се затвори със команда за синхронизация, която да бъде върху обекта obj. Фактически използването на wait() и notify() изисква задължително нишката да притежава ключа за синхронизация върху обекта obj.. В противен случай се връща изключение от типа IllegalMonitorStateException.



Каталог: ~tank -> JAVA -> Lectures
Lectures -> Лекция Изключения. Уравнение на КортевегДеВриз
Lectures -> Лекция 2 Типове данни. Класове и обекти. Числено интегриране
Lectures -> Програма за изчисляване на средна стойност
Lectures -> Лекция аплети и уеб-страници
Lectures -> Програма за изчисляване на средна стойност
Lectures -> Лекция Характерни особености на езика java. Сравнение с други
Lectures -> Приложение Аплети с геометрични фигури. // A java applet displaying a white circle in a black square
Lectures -> Лекция 10. Програмиране в уеб-страница с помощта на Java Script
Lectures -> Лекция Входни и изходни потоци. Писане във файл и четене от


Сподели с приятели:




©obuch.info 2024
отнасят до администрацията

    Начална страница