Slipped Coditionsダイジェスト

7485 ワード

いわゆるSlipped conditionsとは、スレッドがある特定の条件をチェックしてスレッドが動作するまでの間、この条件は他のスレッドによって変更されており、最初のスレッドがこの条件で誤った動作を行ってしまうことを意味する。ここには簡単な例があります。
public class Lock {
    private boolean isLocked = true;

    public void lock(){
      synchronized(this){
        while(isLocked){
          try{
            this.wait();
          } catch(InterruptedException e){
            //do nothing, keep waiting
          }
        }
      }

      synchronized(this){
        isLocked = true;
      }
    }

    public synchronized void unlock(){
      isLocked = false;
      this.notify();
    }
}
ロック()法は二つの同期ブロックを含んでいることが分かる。第1の同期ブロックは、isLockedがfalseになるまでwait動作を行い、第2の同期ブロックはisLockedをtrueにセットし、このロックインスタンスをロックして他のスレッドがロック()方法を通過することを避ける。
もしある時点でisLockedがfalseであれば、この時、二つのスレッドが同時にロックにアクセスする方法があると考えられます。最初のスレッドが最初の同期ブロックに入ると、このとき、isLockedがfalseであることが分かります。この場合、第二のスレッドが実行されると、最初の同期ブロックにも入ります。isLockedはfalseであることも分かります。この条件はfalseであることを確認したスレッドが二つとも第二の同期ブロックに進み、isLockedがtrueであることを設定します。
このシーンは、slipped conditionsの例であり、2つのスレッドが同じ条件をチェックして同期ブロックを終了するので、この2つのスレッドが条件を変更する前に、他のスレッドにこの条件をチェックさせる。言い換えれば、条件はあるスレッドによって確認され、この条件は他のスレッドによって変更されました。
slipped conditionsを避けるためには、条件のチェックと設定は原子でなければなりません。つまり、最初のスレッドのチェックと設定条件の間に、他のスレッドがこの条件をチェックすることはありません。
上の問題を解決する方法は簡単です。isLocked=trueという行のコードを最初の同期ブロックに移して、whileサイクルの後に置くだけです。
public class Lock {
    private boolean isLocked = true;

    public void lock(){
      synchronized(this){
        while(isLocked){
          try{
            this.wait();
          } catch(InterruptedException e){
            //do nothing, keep waiting
          }
        }
        isLocked = true;
      }
    }

    public synchronized void unlock(){
      isLocked = false;
      this.notify();
    }
}
今はISLocked条件をチェックして設定します。同じ同期ブロックで原子地に実行されました。
もっと現実的な例
あなたが言うかもしれません。私はこのような弱いコードを書くことができません。slippd conditionsはかなり理論的な問題だと思います。しかし、最初の簡単な例は、slipped conditionsをよりよく示すために用いられるだけである。
空腹と公平の中で実現される公平な錠は、より現実的な例かもしれない。また、ネストチューブのプログラムロックの死の中の幼稚な実現を見てください。その中のネストチューブのロックの問題を解決しようとすれば、slipped conditions問題が発生しやすいです。
まず、ブロックチューブのロックが死んでいる例を見ましょう。
//Fair Lock implementation with nested monitor lockout problem
public class FairLock {
  private boolean isLocked = false;
  private Thread lockingThread = null;
  private List waitingThreads =
            new ArrayList();

  public void lock() throws InterruptedException{
    QueueObject queueObject = new QueueObject();

    synchronized(this){
      waitingThreads.add(queueObject);

      while(isLocked || waitingThreads.get(0) != queueObject){

        synchronized(queueObject){
          try{
            queueObject.wait();
          }catch(InterruptedException e){
            waitingThreads.remove(queueObject);
            throw e;
          }
        }
      }
      waitingThreads.remove(queueObject);
      isLocked = true;
      lockingThread = Thread.currentThread();
    }
  }

  public synchronized void unlock(){
    if(this.lockingThread != Thread.currentThread()){
      throw new IllegalMonitorStateException(
        "Calling thread has not locked this lock");
    }
    isLocked      = false;
    lockingThread = null;
    if(waitingThreads.size() > 0){
      QueueObject queueObject = waitingThread.get(0);
      synchronized(queueObject){
        queueObject.notify();
      }
    }
  }
}
public class QueueObject {}
synchronizedとその中のqueueObject.wait()呼び出しはsynchronizedブロックに埋め込まれています。これは入れ子のプログラムロックが問題になります。この問題を避けるためには、synchronizedブロックをsynchronizedブロックから取り除く必要があります。移した後のコードはこのようなものかもしれません。
//Fair Lock implementation with slipped conditions problem
public class FairLock {
  private boolean isLocked = false;
  private Thread lockingThread  = null;
  private List waitingThreads =
            new ArrayList();

  public void lock() throws InterruptedException{
    QueueObject queueObject = new QueueObject();

    synchronized(this){
      waitingThreads.add(queueObject);
    }

    boolean mustWait = true;
    while(mustWait){

      synchronized(this){
        mustWait = isLocked || waitingThreads.get(0) != queueObject;
      }

      synchronized(queueObject){
        if(mustWait){
          try{
            queueObject.wait();
          }catch(InterruptedException e){
            waitingThreads.remove(queueObject);
            throw e;
          }
        }
      }
    }

    synchronized(this){
      waitingThreads.remove(queueObject);
      isLocked = true;
      lockingThread = Thread.currentThread();
    }
  }
}
注意:ロックの仕方だけを変えましたので、ここではロックの方法しか見せません。
現在のロック方式は3つの同期ブロックを含んでいる。
最初に、synchronizedブロックはmustWait = isLocked || waitingThreads.get(0) != queueObjectを介して内部変数の値を検査する。
二つ目は、synchronizedブロックがスレッドを待つ必要があるかどうかを確認します。他のスレッドはこの時点でロックが解除された可能性もありますが、この問題はしばらく考えていません。私たちはこのロックがロック解除状態にあると仮定して、スレッドはすぐにsynchronizedブロックを脱退します。
三つ目は、synchronizedブロックがmust Waitがfalseであるときのみ実行されます。これはisLockedをtrueにリセットしてロックから離れる方法です。
ロックがロック解除された状態で、二つのスレッドが同時にロック()を呼び出すとどうなるかを想定します。まず、スレッド1はisLockedがfalseであることを確認し、スレッド2は同様にisLockedがfalseであることを確認する。そして、それらは待つことなく、isLockedをtrueに設定します。これはslipped conditionsの一番いい例です。
Slipped Coditions問題を解決します。
上記の例のslip ped conditions問題を解決するには、最後のsynchronizedブロックのコードは最初の同期ブロックに上に移動しなければなりません。このような変動に適応するために、コードは少し変更する必要があります。以下は変更されたコードです。
//Fair Lock implementation without nested monitor lockout problem,
//but with missed signals problem.
public class FairLock {
  private boolean isLocked = false;
  private Thread lockingThread  = null;
  private List waitingThreads =
            new ArrayList();

  public void lock() throws InterruptedException{
    QueueObject queueObject = new QueueObject();

    synchronized(this){
      waitingThreads.add(queueObject);
    }

    boolean mustWait = true;
    while(mustWait){
      synchronized(this){
        mustWait = isLocked || waitingThreads.get(0) != queueObject;
        if(!mustWait){
          waitingThreads.remove(queueObject);
          isLocked = true;
          lockingThread = Thread.currentThread();
          return;
        }
      }     

      synchronized(queueObject){
        if(mustWait){
          try{
            queueObject.wait();
          }catch(InterruptedException e){
            waitingThreads.remove(queueObject);
            throw e;
          }
        }
      }
    }
  }
}
局所変数must Waitのチェックは、同じ同期ブロックで行われていることが分かります。また、synchronized(this)ブロックの外でmust Waitをチェックしても、while(must Wait)サブルーチンでは、must Wait変数は、synchronized(this)同期ブロックの外に割り当てられたものではないことが見られます。スレッドがmust Waitがfalseであることを確認すると、自動的に内部の条件を設定しますので、他のスレッドがこの条件を調べにきます。この条件の値はtrueです。
synchronizedブロックのreturn;文は必須ではない。これは小さな最適化にすぎない。もしスレッドが待っていないなら、synchronized同期ブロック中にif子文を実行する必要はない。
注意深い読者は上の公平ロックが実現しても信号が失われる可能性があることに気づくかもしれません。このFairLockの例がロック状態にある場合、ロック(lock)方法を呼び出すスレッドがあることを想定する。最初のsynchronizedブロックを実行した後、must Wait変数の値はtrueです。ロックを呼び出すスレッドは占有式で、ロックを持つスレッドはこの時点でロック()を呼び出しますが、これまでのunlock()の実装を見てみると、queue Object.notify()を呼び出しています。しかし、ロックのスレッドがまだ呼び出されていないので、queue Object.wait()は、queueObject.notify()の呼び出しが効かなくなり、信号がなくなってしまいました。ロック()を呼び出すスレッドが他のスレッドでqueueObject.notify()を呼び出した後、queueObject.wait()を呼び出すと、このスレッドは他のスレッドがunlockメソッドを呼び出すまでブロックされますが、これは永遠に発生しません。
公平ロックによって実現される信号の紛失問題は、飢餓と公平の文で議論されています。Que Objectを信号量に変換し、2つの方法を提供しています。dowait()とdoNotify()。これらの方法はQueue Object内部で信号を格納し、応答します。このようにして、doNotify()がdoWait()の前に呼び出されても、信号は失われない。
原文Slipped Coditionsの作者Jakob Jenkov翻訳者の余紹亮via ife