Javaマルチスレッド - ロックの詳細説明とサンプルコード

高洛峰
リリース: 2017-01-05 15:30:30
オリジナル
1264 人が閲覧しました

Java 5 以降、java.util.concurrent.locks パッケージにはいくつかのロック実装が含まれているため、独自のロックを実装する必要はありません。ただし、これらのロックの使用方法を理解する必要があります。

単純なロック

Java の synchronized ブロックから始めましょう:

public class Counter{
  private int count = 0;
 
  public int inc(){
    synchronized(this){
      return ++count;
    }
  }
}
ログイン後にコピー

inc() メソッドに synchronized(this) コード ブロックがあることがわかります。このコード ブロックは、同時に 1 つのスレッドだけが return ++count を実行できることを保証します。 synchronized 同期ブロック内のコードはより複雑になる可能性がありますが、スレッド同期の意味を表現するには ++count のような単純な操作で十分です。

次の Counter クラスは、同じ目的を達成するために同期の代わりに Lock を使用します:

public class Counter{
  private Lock lock = new Lock();
  private int count = 0;
 
  public int inc(){
    lock.lock();
    int newCount = ++count;
    lock.unlock();
    return newCount;
  }
}
ログイン後にコピー

lock() メソッドは Lock インスタンス オブジェクトをロックするため、オブジェクトの lock() メソッドを呼び出すすべてのスレッドは、ロックが完了するまでブロックされます。 Lock オブジェクトの lock() メソッドが呼び出されます。

Lock クラスの簡単な実装を次に示します。

public class Counter{
public class Lock{
  private boolean isLocked = false;
 
  public synchronized void lock()
    throws InterruptedException{
    while(isLocked){
      wait();
    }
    isLocked = true;
  }
 
  public synchronized void unlock(){
    isLocked = false;
    notify();
  }
}
ログイン後にコピー

「スピン ロック」とも呼ばれる while(isLocked) ループに注目してください。 isLocked が true の場合、lock() を呼び出すスレッドは wait() 呼び出しの待機をブロックします。スレッドが notify() 呼び出しを受信せずに wait() から戻ること (誤ったウェイクアップとも呼ばれます) を防ぐために、スレッドは isLocked 条件を再チェックして、実行を継続しても安全かどうか、または実行を継続しても安全かどうかを判断します。再度待機する必要があり、スレッドが目覚めた後も安全に実行を継続できるとは考えられません。 isLocked が false の場合、現在のスレッドは while(isLocked) ループを終了し、isLocked を true に戻し、lock() メソッドを呼び出している他のスレッドが Lock インスタンスをロックできるようにします。

スレッドがクリティカルセクション(lock()とunlock()の間)のコードを完了すると、unlock()が呼び出されます。 lock() を実行すると、isLocked が false にリセットされ、lock() メソッドの wait() 関数の呼び出し後に待機状態になっていたスレッド (存在する場合) の 1 つが通知 (ウェイクアップ) されます。

ロックの再入可能

Java の同期ブロックは再入可能です。これは、Java スレッドがコード内の同期された同期ブロックに入り、その同期ブロックで使用される同期オブジェクトに対応するモニターのロックを取得した場合、このスレッドは同じモニター オブジェクトによって同期された別のブロックに入ることができることを意味します。 Javaコード。以下に例を示します。

public class Reentrant{
  public synchronized outer(){
    inner();
  }
 
  public synchronized inner(){
    //do something
  }
}
ログイン後にコピー

outer() と inner() の両方が synchronized として宣言されていることに注意してください。これは、Java の synchronized(this) ブロックに相当します。スレッドが external() を呼び出す場合、両方のメソッド (コードのブロック) が同じモニター オブジェクト (「this」) によって同期されるため、outer() 内で inner() を呼び出しても問題はありません。スレッドがすでにモニター オブジェクトのロックを所有している場合、スレッドはモニター オブジェクトによって同期されたすべてのコード ブロックにアクセスできます。これがリエントランシーです。スレッドは、すでに所有しているロックによって同期されているコードの任意のブロックに入ることができます。

前に示したロックの実装は再入可能ではありません。 Reentrantクラスを以下のように書き換えると、スレッドがouter()を呼び出すと、inner()メソッドのlock.lock()でブロックされてしまいます。

public class Reentrant2{
  Lock lock = new Lock();
 
  public outer(){
    lock.lock();
    inner();
    lock.unlock();
  }
 
  public synchronized inner(){
    lock.lock();
    //do something
    lock.unlock();
  }
}
ログイン後にコピー

outer() を呼び出すスレッドは、まず Lock インスタンスをロックし、次に inner() を呼び出し続けます。 inner() メソッドでは、スレッドは Lock インスタンスを再度ロックしようとしますが、Lock インスタンスは既に external() メソッドでロックされているため、アクションは失敗します (つまり、スレッドはブロックされます)。

2 つの lock() 呼び出しの間に lock() が呼び出されないと、lock() の実装を確認すると、その理由が明らかになることがわかります:

public class Lock{
  boolean isLocked = false;
 
  public synchronized void lock()
    throws InterruptedException{
    while(isLocked){
      wait();
    }
    isLocked = true;
  }
 
  ...
}
ログイン後にコピー

スレッドが許可されているかどうか。 lock() を終了する方法は、while ループ (スピン ロック) 内の条件によって決まります。現在の判定条件は、どのスレッドがロックしたかに関係なく、isLocked が false の場合にのみロック操作が許可されます。

この Lock クラスを再入可能にするには、それに小さな変更を加える必要があります:

public class Lock{
  boolean isLocked = false;
  Thread lockedBy = null;
  int lockedCount = 0;
 
  public synchronized void lock()
    throws InterruptedException{
    Thread callingThread =
      Thread.currentThread();
    while(isLocked && lockedBy != callingThread){
      wait();
    }
    isLocked = true;
    lockedCount++;
    lockedBy = callingThread;
 }
 
  public synchronized void unlock(){
    if(Thread.curentThread() ==
      this.lockedBy){
      lockedCount--;
 
      if(lockedCount == 0){
        isLocked = false;
        notify();
      }
    }
  }
 
  ...
}
ログイン後にコピー

while ループ (スピン ロック) が、Lock インスタンスをロックしたスレッドも考慮するようになったことに注意してください。現在のロック オブジェクトがロックされていない (isLocked = false)、または現在の呼び出しスレッドが既に Lock インスタンスをロックしている場合、while ループは実行されず、lock() を呼び出しているスレッドはメソッドを終了できます (翻訳注:現在のセマンティクスでは、「このメソッドの終了が許可される」ということは、wait() が呼び出されず、ブロッキングが発生しないことを意味します。

さらに、同じスレッドがロック オブジェクトを繰り返しロックした回数を記録する必要があります。それ以外の場合は、現在のロックが複数回ロックされている場合でも、1 回の unblock() 呼び出しでロック全体がロック解除されます。対応する lock() 呼び出しの数に unlock() 呼び出しが達する前にロックが解放されることは望ましくありません。

これで、この Lock クラスは再入可能になりました。

ロックの公平性

Java の同期ブロックは、ブロックに入ろうとするスレッドの順序を保証しません。したがって、複数のスレッドが同じ同期ブロックへのアクセスを求めて常に競合する場合、そのうちの 1 つまたは複数のスレッドがアクセスを取得できない危険性があります。つまり、アクセスは常に他のスレッドに割り当てられることになります。この状況はスレッドの飢餓と呼ばれます。この問題を回避するには、ロックが公平である必要があります。この記事で紹介されているロックは、同期されたブロックを使用して内部で実装されているため、公平性は保証されません。

在 finally 语句中调用 unlock()

如果用 Lock 来保护临界区,并且临界区有可能会抛出异常,那么在 finally 语句中调用 unlock()就显得非常重要了。这样可以保证这个锁对象可以被解锁以便其它线程能继续对其加锁。以下是一个示例:

lock.lock();
try{
  //do critical section code,
  //which may throw exception
} finally {
  lock.unlock();
}
ログイン後にコピー

这个简单的结构可以保证当临界区抛出异常时 Lock 对象可以被解锁。如果不是在 finally 语句中调用的 unlock(),当临界区抛出异常时,Lock 对象将永远停留在被锁住的状态,这会导致其它所有在该 Lock 对象上调用 lock()的线程一直阻塞。

以上就是关于 java 多线程锁的资料整理,后续继续补充相关资料,谢谢大家对本站的支持!


更多java 多线程-锁详解及示例代码相关文章请关注PHP中文网!


関連ラベル:
ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート