* Wait, notify и notifyAll
Публикувано на 19 октомври 2009 в раздел ПИК3 Java.
Вече се запознахме с методът sleep() за нишки в Java, както и възможността да прекъснем "спането" на нишката чрез метод interrupt(). Използването на метод sleep() всъщност прехвърля текущата нишка в "Not Runnable" статус за определен период от време и по този начин дава процесорно време на другите нишки. Важно е да се спомене, че ако методът, който е извикал sleep(), е синхронизиран (synchronized), никой не може да достъпи обектите в него по време на неговия "sleep" период! Извикването на "interrupt()" за тази нишка ще прекъсне sleep() преждевременно.
Когато имаме обект (говорим за който и да е обект създаден с оператор new), разполагаме с нестатичен метод Object.wait(). Този метод на пръв поглед предизвиква същия ефект както Thread.sleep() - прехвърля текущата нишка (тази, която е извикала метода) в "Not Runnable" статус за определено време. Такъв обект се нарича "заключващ обект" за нишката. Първата разлика е, че Object.wait() може да бъде извикан само в синхронизиран метод. Втората разлика е, че Object.wait() може да приспи нишката за неопределено време, докато Thread.sleep() е с фиксирано. Освен това sleep() е статичен метод - по-правилно е да го извикваме чрез Thread.sleep(), а не чрез инстанция на обект.
Преди да дадем пример трябва да кажем, че wait() и notify() са final методи за клас Object. Понеже Object е базов шаблон за клас, който всеки наследява по подразбиране, всеки един клас в Java ги притежава.
В първият пример ще демонстрираме как можем да накараме една нишка да "заспи" докато не се извършат необходимите стартирани в нея изчисления:
public class waitNotifyExample{ public static void main(String[]args){ ThreadExample T = new ThreadExample(); T.start(); synchronized(T){ System.out.println("Waiting until calculations finish..."); try{ T.wait(); } catch (InterruptedException e){} } System.out.println("Result: "+T.sum); } } class ThreadExample extends Thread{ public static int sum=0; public void run(){ synchronized (this){ this.sum(); System.out.println("Finished summing. Now the program can continue..."); this.notify(); } } public void sum(){ System.out.println("Suming the first 500 integers..."); for(int i=1; i<=500; i++){ this.sum = this.sum+i; } } }
Преди да бъде извикан "wait()" методът за обекта, нишката е длъжна да се синхронизира по него (т.е. други методи не могат да го модифицират). След това цялата нишка се "приспива" в т.нар. "wait list" (от примера по-горе приспахме главната нишка на main метода). В последствие друга нишка има възможност да "събуди" обекта чрез метод "notify()" (това направи самата нишка, по която бяхме приспали main метода).
В този пример показахме как главната нишка (main) извиква друга нишка и изчаква докато тя си свърши работата. Нека погледнем и обратния вариант - главната нишка стартира нова нишка и новата нишка изчаква докато главната не я уведоми:
public class myfirstprogram { public static void main(String args[]) throws Exception { System.out.println("Starting thread..."); MyThread T = new MyThread("MyThread"); System.out.println("Main thread printing dots..."); for (int i = 0; i < 50; i++) { Thread.sleep(50); System.out.print("."); } T.start(); } } class MyThread implements Runnable{ boolean ready; Thread T; String name; MyThread(String name){ this.ready = false; this.name = name; this.T = new Thread(this, name); this.T.start(); } synchronized void suspendThread(){ System.out.println(this.name+" suspended until main finishes..."); while (!ready){ try{ this.wait(); } catch(InterruptedException e){} } System.out.println("\n"+this.name+" is now resumed!"); } synchronized void start(){ ready = true; notify(); } public void run() { this.suspendThread(); } }
Нека сега покажем още един пример, в който две нишки се "блокират" една друга докато изпълнението на съответните методи не приключи. Имаме два класа и две инстанции - обект, който внася пари и обект, който ги прибира. Нека погледнем следният пример:
public class WaitNotifyExample{ public static void main(String args[]) { MoneyQueue q = new MoneyQueue(); MoneyGiver mg = new MoneyGiver(q, "MoneyGiver"); MoneyFetcher mf = new MoneyFetcher(q, "MoneyFetcher"); } } class MoneyQueue{ double moneyTransfer; boolean haveMoneyInAccount = false; synchronized double getMoney(String name) { if(haveMoneyInAccount == false){ try { this.wait(); } catch(InterruptedException e) { System.out.println("This should not happen in this program"); } } System.out.println(name +" fetched " + moneyTransfer); haveMoneyInAccount = false; this.notify(); return moneyTransfer; } synchronized void addMoney(){ if(haveMoneyInAccount == true){ try { this.wait(); } catch(InterruptedException e) { System.out.println("This should not happen in this program"); } } System.out.print("Add money: "); java.util.Scanner s = new java.util.Scanner(System.in); double moneyTransfer = s.nextDouble(); this.moneyTransfer = moneyTransfer; haveMoneyInAccount = true; this.notify(); } } class MoneyGiver implements Runnable{ String name; Thread T; MoneyQueue q; public MoneyGiver(MoneyQueue q, String name){ this.q = q; this.name = name; T = new Thread(this, "MoneyGiver"); T.start(); } public void run() { while(true){ q.addMoney(); } } } class MoneyFetcher implements Runnable{ double money; String name; Thread T; MoneyQueue q; public MoneyFetcher(MoneyQueue q, String name) { this.q = q; this.name = name; money = 0; T = new Thread(this, "MoneyFetcher"); T.start(); } public void run() { while(true) { this.money += q.getMoney(this.name); System.out.println(this.name+" now have "+this.money); } } }
MoneyQueue е клас, в който са дефинирани два синхронизирани метода - addMoney и getMoney. Освен това създаваме два обекта - MoneyGiver (вкарващ пари в системата) и MoneyFetcher (взимащ тези пари и натрупващ ги в собствената си сметка).
Идеята на примера е следната - в началото в MoneyQueue няма никакви пари, което е описано чрез променливата haveMoneyInAccount. Двата обекта MoneyGiver и MoneyFetcher се стартират в две нишки. MoneyFetcher моментално се опитва да вземе пари, но тъй като такива няма, той изпада в спящ режим (в метод getMoney() изпадаме в статус wait(), с което извикващия метод започва да чака). При MoneyGiver ситуацията е точно обратната - виждаме, че няма пари в опашката и затова се поисква въвеждане на такива от клавиатурата. В момента в който вкараме пари в опашката, haveMoneyInAccount става true и се извиква this.notify(), с което "събуждаме" другата нишка. MoneyGiver моментално ще извика същия метод (addMoney) отново, но този път ще забележи, че все още има пари за взимане в опашката и затова ще изпадне в wait() статус (докато парите бъдат взети). MoneyFetcher вече е "събуден" - той ще вземе парите от опашката и ще укаже, че в нея вече няма пари (тоест при следващо извикване на същия метод той самия пак ще е в wait() статус). Естествено отново се извиква this.notify(), за да "събудим" обекта MoneyGiver. Тази поредност в случая може да се повтаря до безкрайност.
Методът notifyAll() е по-специален - той "събужда" всички нишки, които са попаднали в wait() статус. По принцип когато пишете програми трябва много добре да прецените колко нишки евентуално могат да заспят. Ако е само една, notify() е достатъчен. Ако са много, е по-вероятно да ви трябва notifyAll(). Ако използвате notify() и повече от една нишка заспи, рискувате нишките ви да "зациклят". Въпреки това не препоръчваме тактиката да използвате винаги notifyAll() - ако това ви се налага, но не е очаквано, по-добре си напишете програмата по-добре.
Обикновено използваме notifyAll() когато нишките имат споделен ресурс по който те се заключват:
public class myfirstprogram { public static void main(String args[]) throws Exception { ThreadsResource tr = new ThreadsResource(); MyThread T1 = new MyThread("MyThread1", tr); MyThread T2 = new MyThread("MyThread2", tr); for (int i = 0; i < 50; i++){ Thread.sleep(50); System.out.print("."); } System.out.println(); tr.resume(); } } class ThreadsResource{ boolean ready = false; synchronized void suspendThread(){ System.out.print(Thread.currentThread().getName()); System.out.println(" suspended until main finishes..."); while(!ready){ try{ this.wait(); } catch(InterruptedException e){} } System.out.println(Thread.currentThread().getName()+" is now resumed!"); } synchronized void resume() { ready = true; this.notifyAll(); } } class MyThread implements Runnable{ ThreadsResource tr; Thread T; MyThread(String name, ThreadsResource tr){ this.tr = tr; this.T = new Thread(this, name); this.T.start(); } public void run() { tr.suspendThread(); } }
Виждате, че когато споделеният ресурс ThreadsResource извика "notifyAll()", всички нишки, които са "заспали" чрез него се "събуждат". Това например е често използвана тактика при многопотребителски игри, при които синхронизацията е важна.
В ThreadsResource може ли wait() да не е в while цикъла? Нали и без това нишките ще чакат докато не бъде извикан notifyAll() ?
Забравил съм какво съм писал тук. На първо четене даже въобще не трябва да има цикъл :)
Да, и аз точно това си мислих. Но сега пък като се зачетох, попаднах на това:
As in the one argument version, interrupts and spurious wakeups are possible, and this method should always be used in a loop:
synchronized (obj) {
while ()
obj.wait();
... // Perform action appropriate to condition
}
Така че, може би си е правилно.
Тук не е обяснено много добре. sleep() е статичен метод на клас Thread и винаги приспива текущата нишка т.е. ако имаме Thread t, то t.sleep() ще приспи нишката, която е извикала метода, а не нишката t. Затова, за да не става объркване, най-добре е sleep() да се извиква с името на класа - Thread.sleep() :)
Здравейте, пиша относно разликата между методите sleep() и wait(). Не съм сигурна дали я разбрах коректно, затова въпросът ми е следния: ако примерно в метод main извикаме T.sleep(), ще приспим самата нишка Т, а ако извикаме Т.wait() ще приспим самия метод main?
Ако нишка A извика b.wait(), то нишка A се приспива и чака обект b да я събуди с notify()
Sleep приспива нишката винаги за определено време. Там не се чака notify, а се чака изтичане на срока на приспиване.
Ясно, благодаря.
Ако махнем synchronized(this) и this.notify от класа ThreadExample при първата програма, тя пак се изпълнява както трябва, т.е. нишката се "събужда" без да се използва notify(). Може ли да обясните защо е така?
Ако махнеш synchronized в случая няма проблем, защото няма втора нишка, която да "пипа" този обект. Всъщност никоя не прави нищо по него и затова нищо не може да се счупи. А колкото до notify - без него продължава да работи, защото run() метода свършва и нишката приключва своето действие. Когато една нишка се затваря се извиква нейния notifyAll метод автоматично.
Въпроса ми е как работи run() метода, след като нишката в main() е "заспала"?
T.wait() кара main да заспи и да изчака T да му даде notify.