C, PHP, VB, .NET

Дневникът на Филип Петров


* 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()“, всички нишки, които са „заспали“ чрез него се „събуждат“. Това например е често използвана тактика при многопотребителски игри, при които синхронизацията е важна.

 



11 коментара


  1. Someone каза:

    В ThreadsResource може ли wait() да не е в while цикъла? Нали и без това нишките ще чакат докато не бъде извикан notifyAll() ?

  2. Забравил съм какво съм писал тук. На първо четене даже въобще не трябва да има цикъл :)

  3. Someone каза:

    Да, и аз точно това си мислих. Но сега пък като се зачетох, попаднах на това:
    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
    }
    Така че, може би си е правилно.

  4. Иво каза:

    Тук не е обяснено много добре. sleep() е статичен метод на клас Thread и винаги приспива текущата нишка т.е. ако имаме Thread t, то t.sleep() ще приспи нишката, която е извикала метода, а не нишката t. Затова, за да не става объркване, най-добре е sleep() да се извиква с името на класа – Thread.sleep() :)

  5. К Ангелова каза:

    Здравейте, пиша относно разликата между методите sleep() и wait(). Не съм сигурна дали я разбрах коректно, затова въпросът ми е следния: ако примерно в метод main извикаме T.sleep(), ще приспим самата нишка Т, а ако извикаме Т.wait() ще приспим самия метод main?

  6. Ако нишка A извика b.wait(), то нишка A се приспива и чака обект b да я събуди с notify()

    Sleep приспива нишката винаги за определено време. Там не се чака notify, а се чака изтичане на срока на приспиване.

  7. К Ангелова каза:

    Ясно, благодаря.

  8. Студент каза:

    Ако махнем synchronized(this) и this.notify от класа ThreadExample при първата програма, тя пак се изпълнява както трябва, т.е. нишката се „събужда“ без да се използва notify(). Може ли да обясните защо е така?

  9. Ако махнеш synchronized в случая няма проблем, защото няма втора нишка, която да „пипа“ този обект. Всъщност никоя не прави нищо по него и затова нищо не може да се счупи. А колкото до notify – без него продължава да работи, защото run() метода свършва и нишката приключва своето действие. Когато една нишка се затваря се извиква нейния notifyAll метод автоматично.

  10. Студент каза:

    Въпроса ми е как работи run() метода, след като нишката в main() е „заспала“?

  11. T.wait() кара main да заспи и да изчака T да му даде notify.

Добави коментар

Адресът на електронната поща няма да се публикува


*