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