一歩一歩スレッドメカニズムを把握する(五)---待機と通知メカニズム

10669 ワード

以前、Threadの停止に関する議論では、doneのタグを設定する方法を使用していましたが、doneがtrueに設定されるとスレッドは終了し、falseに設定されるとスレッドは永遠に実行されます.これにより多くのCPUサイクルが消費され、メモリに友好的ではない動作になります.
      JAva内のオブジェクトはロックだけでなく、関連メソッドを呼び出すことで待機者や通知者になることができます.
     Objectオブジェクト自体にはwait()とnotify()の2つの方法があります.wait()は条件の発生を待機し、notify()は待機中のスレッドにこの条件が発生したことを通知します.synchronizedメソッドまたはブロックから呼び出さなければなりません.
     このような待機通知メカニズムの目的はいったいなぜなのか.
     待機通知メカニズムは同期メカニズムですが、あるスレッドと別のスレッドを特定の条件下で通信できる通信メカニズムのようです.しかし,このメカニズムは特定の条件が何であるかを指定していない.
     待機-通知メカニズムはsynchronizedメカニズムに取って代わることができますか?もちろんだめです.待機通知メカニズムはsynchronizedメカニズムが解決できる競争問題を解決するものではありません.実際には、両者は互いに協力して使用されており、それ自体にも競争問題があります.これはsynchronziedによって解決する必要があります.
private boolean done = true;

public synchronized void run(){
      while(true){
            try{
                 if(done){
                       wait();
                 }else{
                       repaint();
                       wait(100);
                 }
            }catch(InterruptedException e){
                  return;
            }
      }
}

public synchronized void setDone(boolean b){
     done = b;
     if(timer == null){
          timer = new Thread(this);
          timer.start();
     }
     if(!done){
          notify();
     }
}

     ここのdoneはvolatileではありません.タグ値を設定するだけでなく、タグを設定しながら自動的に通知を送信する必要があります.だから、私たちは今synchronizedを通じてdoneへのアクセスを保護しています.     run()メソッドはdoneがfalseの場合に自動的に終了しません.wait()メソッドを呼び出すことで、他のスレッドがnotify()メソッドを呼び出すまでスレッドをこのメソッドで待機させます.
     ここには私たちの注意に値するところがいくつかあります.
     まず、ここではsleep()メソッドではなくwait()メソッドを使用してスレッドをスリープさせる.wait()メソッドは、スレッドがオブジェクトの同期ロックを持つ必要があるため、wait()メソッドが実行されるとロックが解放され、通知が受信されると、スレッドはwait()メソッドが戻る前にロックを再取得する必要があり、常にロックを持っているようにする.このテクニックは、設定と送信通知およびテストと取得通知の間に競合があるため、wait()とnotify()が同期ロックを保持しながら呼び出されなければ、この通知が受信されることを保証することは全くできず、wait()メソッドが待機する前にロックを解除しなければ、notify()メソッドを呼び出すことはできない.ロックを取得できないため、これもsleep()ではなくwait()を使用するもう一つの理由です.sleep()メソッドを使用すると、このロックは解放されず、setDone()メソッドも実行されず、通知も送信されません.
     次にrun()を同期化する.以前に議論したように,run()を同期することは非常に危険であり,run()法は絶対に完成しない,すなわちロックは永遠に解放されないが,wait()自体がロックを解放するので,この問題も回避される.
     notify()メソッドが呼び出されると、スレッドが待機していないのではないかという疑問があります.
     待機-通知メカニズムは、送信された通知の条件を知らないため、スレッドが待機していないときに通知が受信されていないと仮定します.この場合、通知も返され、通知も失われます.後でwait()メソッドを実行するスレッドは、別の通知を待たなければなりません.
     待機-通知メカニズム自体にも競争問題があると述べたが、これは皮肉なことに、同期問題を解決するために使われていたメカニズム自体にも同期問題がある.実は、競争は必ずしも問題ではなく、問題を起こさなければいいのです.ここで競争問題を分析します.
     wait()を使用するスレッドは、条件が存在しないことを確認します.これは通常、変数をチェックすることによって実現され、wait()メソッドを呼び出します.他のスレッドがこの条件を設定した場合、notify()メソッドは通常、同じ変数を設定することによって呼び出されます.競合は、次のような状況で発生します.
1.最初のスレッドは条件をテストし、待つ必要があることを確認します.
2.第2スレッドはこの条件を設定する.
3.第2のスレッドはnotify()メソッドを呼び出します.これは、第1のスレッドがまだ待機中であるため、受信されません.
4.最初のスレッドはwait()メソッドを呼び出します.
      この競合は、同期ロックによって実現される必要があります.条件の検査と設定がautomicであることを確保するためにロックを取得しなければなりません.つまり、検査と設定はロックの範囲内でなければなりません.
      上述したように、wait()メソッドはロックを解放してロックを再取得するので、この間に競合が発生するのではないでしょうか.理論的にはありますが、システムはこのような状況を阻止します.wait()メソッドはロックメカニズムと密接に結合されており,待機しているスレッドが通知を受信できる準備ができている状態に入るまで,オブジェクトのロックは実際には解放されない.
      私たちの疑問はまだ続いています:スレッドは通知を受け取って、条件が正しい設定を保証することができますか?申し訳ありませんが、答えは違います.wait()メソッドを呼び出す前に、スレッドは常に同期ロック時のテスト条件を有し、wait()メソッドから戻ると、他のスレッドも同様に条件をテストして待つ必要がないと判断し、通知を発行したスレッドによって設定された有効なデータを処理するため、このスレッドは常に条件を再テストして待つ必要があるかどうかを判断しなければならない.しかし、これは1つのスレッドだけが通知を待っている場合であり、複数のスレッドが通知を待っている場合、競合が発生し、これは通知の紛失を防ぐために内部の競合だけを解決できるため、通知メカニズムでは解決できません.マルチスレッド待機の最大の問題は、1つのスレッドが他のスレッドで通知を受信した後に通知を受信すると、この通知が有効であることを保証できないため、待機しているスレッドはステータスをチェックするためのオプションを提供し、通知が処理された場合に待機状態に戻る必要があります.これも、wait()をループに常に入れる理由です.
      wait()もスレッドが中断されたときに早めに戻り、私たちのプログラムも中断を処理しなければなりません.
      マルチスレッド通知では、正しいスレッドが通知を受信することをどのように確保しますか?答えはだめです.私たちはどのスレッドが通知を受け取ることができるか保証できません.できる方法は、すべての待機しているスレッドが通知を受け取ることです.これはnotifyAll()によって実現されますが、すべての待機しているスレッドを本当に呼び覚ますわけではありません.ロックの問題で、実質的にすべてのスレッドが呼び覚まされます.しかし、実際に実行されているスレッドは1つしかありません.
       このようにするのは、1つ以上の条件が待機しているためかもしれません.どのスレッドが起動するかを確保できない以上、すべてのスレッドを起動し、条件に基づいて実行するかどうかを自分で判断します.
       待機-通知メカニズムはsynchronizedと組み合わせて使用できます.
private Object doneLock = new Object();

public void run(){
     synchronized(doneLock){
           while(true){
                if(done){
                      doneLock.wait();
                }else{
                      repaint();
                      doneLock.wait(100);
                }
           }catch(InterruptedException e){
                 return;
           }
     }
}

public void setDone(boolean b){
     synchronized(doneLock){
          done = b;
          if(timer == null){
               timer = new Thread(this);
               timer.start();
          }
          if(!done){
                doneLock.notify();
          }
     }
}

     このテクニックは、特に、同じ時間により多くのスレッドを異なる方法にアクセスさせることができるため、オブジェクトロックに対する競合において非常に有用である.     最後に説明するのは条件変数です.
     J 2 SE 5.0はConditionインタフェースを提供している.Conditionインタフェースはロックインタフェースにバインドされ、待機通知メカニズムが同期ロックにバインドされているようにします.
private Lock lock = new ReentrantLock();
private Condition  cv = lockvar.newCondition();

public void run(){
     try{
          lock.lock();
          while(true){
               try{
                   if(done){
                         cv.await();
                   }else{
                         nextCharacter();
                         cv.await(getPauseTime(), TimeUnit.MILLISECONDS);
                   }
               }catch(InterruptedException e){
                     return;
               }
          }
     }finally{
           lock.unlock();
     }
}

public void setDone(boolean b){
    try{
         lock.lock();
         done = b;
         if(!done){
               cv.signal();
         }finally{
               lock.unlock();
         }
    }
}

    上記の例では、別の方法で私たちの前の待機-通知メカニズムを完了するように見えますが、実際に条件変数を使用するにはいくつかの理由があります.1.条件変数はロックオブジェクトを使用する際に必要です.ロックオブジェクトのwait()とnotify()は動作しないため、これらの方法は内部でロックオブジェクトを実現するために使用されているため、より重要なのは、ロックオブジェクトを持つことは、ロックオブジェクトとオブジェクトに関連付けられた同期ロックが異なるため、そのオブジェクトを持つ同期ロックを意味しません.
2.Conditionオブジェクトはjavaの待ち-通知メカニズムとは異なり、異なるオブジェクトとして作成され、ロックオブジェクトごとに1つ以上のConditionオブジェクトを作成することができるので、個別のスレッドやスレッドのグループに対して独立した設定を行うことができます.つまり、同じオブジェクト上で同期化された待機中のすべてのスレッドに対して同じ条件を待たなければなりません.
      基本的に、Conditionインタフェースの方法は、レプリケーション待機通知メカニズムであるが、中断を回避したり、相対的または絶対的な時間で期限を指定したりすることができる便利さを提供する.