Java同期の詳細-スレッドロックと条件オブジェクト

9207 ワード

ワイヤロックと条件オブジェクト
ほとんどのマルチスレッドアプリケーションでは、2つ以上のスレッドが同じデータへのアクセスを共有する必要があるため、2つのスレッドが同じリソースに同時にアクセスする場合があります.これを競合条件と呼びます.
Javaでは同時データアクセスの問題を解決するために,ロックという概念を用いて解決するのが一般的である.
コードの同時アクセスによる干渉を防止するメカニズムはいくつかあります.
1.synchronizedキーワード(ロックと関連条件を自動的に作成)
2.ReentrantLockクラス+Java.util.concurrentパッケージのlockインタフェース(Java 5.0の場合に導入)
ReentrantLockの使用

public void Method() {
    boolean flag = false;//    
    ReentrantLock locker = new ReentrantLock();
    locker.lock();//     
    try {
      //do some work...
    } catch (Exception ex) {

    } finally {
      locker.unlock();//    
    }
  }

locker.lock();1つのスレッドのみが臨界領域に入ることを確認し、1つのスレッドが入るとロックオブジェクトが得られ、他のスレッドはlock文を通過できません.他のスレッドがlockを呼び出すと、最初のスレッドがロックオブジェクトを解放することを知ってブロックされます.
locker.unlock();ロック解除操作はfinallyに必ず入れなければなりません.try文に問題が発生した場合、ロックは解放されなければなりません.そうしないと、他のスレッドは永遠にブロックされます.
システムはランダムにスレッドにリソースを割り当てるので、スレッドがロック対象を取得した後、システムに実行権を奪われる可能性があります.この場合、他のスレッドがアクセスしますが、ロックがあり、入れないことがわかりました.ロック対象を取得したスレッドが中のコードを実行した後、ロックを解除して、2番目のスレッドが実行できます.
銀行振替の機能を行うと仮定すると、スレッドロック操作は銀行クラスの振替方法に定義されるべきである.このように各銀行オブジェクトにロックオブジェクトがあり、2つのスレッドが1つの銀行オブジェクトにアクセスすると、ロックはシリアル方式でサービスを提供するからである.ただし、各スレッドが異なる銀行オブジェクトにアクセスすると、各スレッドは異なるロックオブジェクトを取得し、互いに衝突しないため、不要なスレッドのブロックは発生しません.
ロックは再読み込み可能であり、スレッドはすでに保持されているロックを繰り返し取得することができ、ロックは1つの保持数カウントによってlockメソッドのネストされた使用を追跡する.
仮に、1つのスレッドがロックを取得した後、Aメソッドを実行するが、AメソッドではBメソッドが呼び出され、このスレッドは2つのロックオブジェクトを取得し、スレッドがBメソッドを実行すると、ロックされて他のスレッドの乱入を防止し、Bメソッドの実行が完了すると、ロックオブジェクトは1つになり、Aメソッドも実行が完了すると、ロックオブジェクトは0つになり、スレッドはロックを解除します.
synchronizedキーワード
前述のReentrantLockロックオブジェクトの使用について説明しましたが、システムではReentrantLockロックを使用する必要はありません.Javaでは内部の暗黙的なロックも提供されています.キーワードはsynchronizedです.
例を挙げます.

public synchronized void Method() {
  //do some work...
}

戻り値の前にsynchronizedロックを付けるだけで、上のReentrantLockロックと同様の効果が得られる.
条件オブジェクト
通常、スレッドはロックオブジェクトを取得しますが、下に進むには条件を満たす必要があることがわかります.
銀行プログラムを例に挙げると、振り替え先の口座に十分な資金が必要で、目標口座に振り替えることができます.この場合、ReentrantLock対象を使う必要があります.振り替え先の口座に十分な資金があると判断した後、スレッドが他のスレッドに切断され、他のスレッドが実行された後、振り替え先のお金には十分な資金がありません.この場合、システムが判断を完了したため、下に進み続け、銀行システムに問題が発生します.
例:

public void Transfer(int from, int to, double amount) {
  if (Accounts[from] > amount)//               ,             ,    
    DoTransfer(from, to, amount);
}

ReentrantLockオブジェクトを使用する必要があります.コードを変更します.

public void Transfer(int from, int to, double amount) {
  ReentrantLock locker = new ReentrantLock();
  locker.lock();
  try {
    while (Accounts[from] < amount) {
      //       
    }
    DoTransfer(from, to, amount);
  } catch (Exception ex) {
    ex.printStackTrace();
  } finally {
    locker.unlock();
  }
}

しかし、これにはまた問題があり、現在のスレッドがロック対象を取得した後、コードを実行し始め、お金が足りないことに気づき、待機状態に入り、他のスレッドがロックのせいで口座に振り込めなくなり、待機状態になります.
この問題はどのように解決しますか.
条件対象登场!

public void Transfer(int from, int to, double amount) {
  ReentrantLock locker = new ReentrantLock();
  Condition sufficientFunds = locker.newCondition();//    ,
  lock.lock();
  try {
    while (Accounts[from] < amount) {
      sufficientFunds.await();
      //       
    }
    DoTransfer(from, to, amount);
    sufficientFunds.signalAll();
  } catch (Exception ex) {
    ex.printStackTrace();
  } finally {
    locker.unlock();
  }
}

条件オブジェクトのキーワードは、1つのロックオブジェクトに1つ以上の関連条件オブジェクトを持つことができるConditionです.オブジェクトをロック.newCondition法により条件オブジェクトを得ることができる.
一般的に条件オブジェクトの命名にはその表現の条件を反映できる名前が必要であるため,ここではsufficientFundと呼び,残高が十分であることを意味する.
ロックに入る前に、条件を作成し、金額が不足している場合は、条件オブジェクトのawaitメソッドを呼び出し、システムの現在のスレッドが保留中であることを通知し、他のスレッドに実行させます.これにより、今回の呼び出しがロックされ、システムは再びこの方法を他の口座に振り替えることができます.振り替えが完了するたびに、振り替え操作を実行するスレッドが下部でsignalAllを呼び出して、すべてのスレッドが実行を継続できることを通知します.現在の口座に十分なお金を振り替える可能性があるため、スレッドが実行を継続する可能性があります.(必ずしもあなたではなく、すべてのスレッドに通知します.通知されたスレッドが条件に合わない場合は、awaitメソッドを呼び出し続け、振り替え操作を完了し、他の保留中のスレッドに通知します.
なぜ現在のスレッドに直接通知しないのですか?だめです.signalメソッドを呼び出して1つのスレッドだけを通知することができますが、このスレッドが操作している口座にお金がない場合(この口座に振り込まれた場合ではありません)、このスレッドはまた待機しています.この場合、他のスレッドに通知できるスレッドはありません.プログラムはデッドロックなので、signalを使ったほうが保険です.
以上はReentrantLock+Conditionオブジェクトを使用していますが、synchronized暗黙ロックを使用したらどうしますか?
いいです.そして必要ありません.

public void Transfer(int from, int to, double amount) {
   while (Accounts[from] < amount) {
      wait();//  wait      Object    ,     ,      await  ,    
      //       
    }
    DoTransfer(from, to, amount);
    notifyAll();//         
}

Objectクラスにはwait、notifyAll、notifyメソッドが定義、await、signalAll、signalメソッドに対応し、暗黙的ロックを操作するために使用され、synchronizedには1つの条件しかないが、ReentrantLockが明示的に宣言するロックは複数のCondition条件をバインドすることができる.
どうきブロック
上記の2つのスレッドロックを取得する方法に加えて、同期ブロックと呼ばれる他のメカニズムがロックを取得します.

Object locker = new Object();
synchronized (locker) {
  //do some work
}

//            
sychronized(this){
  //do some work
}

以上のコードは、Objectタイプlockerオブジェクトのロックを取得する、このロックは特殊なロックであり、上記のコードでは、このObjectクラスオブジェクトを作成するのは、単にその持つロックを使用するためだけである.
このメカニズムは同期ブロックと呼ばれ、適用シーンも広い:ある場合、私たちはすべての方法が同期する必要はありません.ただ、方法の一部のコードブロックが同期する必要があります.この場合、私たちはこの方法をsynchronizedと宣言すれば、特に方法が大きい場合、大きなリソースの浪費をもたらします.そのため、この場合、syncを使用することができます.hronizedキーワードは、同期ブロックを宣言します.

public void Method() {
  //do some work without synchronized
  synchronized (this) {
    //do some synchronized operation
  }
}

モニタの概念
ロックと条件は同期において重要なツールですが、オブジェクト向けではありません.長年、Javaの研究者は、どのようにロックするかを考慮する必要がなく、マルチスレッドの安全性を保証する方法を探してきました.最も成功した解決策はmonitorモニタと呼ばれ、このオブジェクトは各Object変数に組み込まれており、かなり1つのライセンスです.ライセンスを取得すれば操作できますが、取得しなければ待機をブロックする必要があります.
モニタには次の機能があります.
1.モニタはプライベートドメインのみを含むクラス
2.各モニタオブジェクトに関連するロックがある
3.モニタオブジェクトのロックを使用してすべての方法をロックする(例を挙げると、obj.Methodメソッドを呼び出すと、objオブジェクトのロックはメソッド呼び出し時に自動的に取得され、メソッドが終了または戻った後に自動的にロックが解放されます.すべてのドメインがプライベートであるため、クラスオブジェクトを操作するときに他のスレッドがアクセスできるドメインがないことを保証できます)
4.ロック対象は、任意の複数の関連条件を有することができる
以上の要件を満たす限り、モニタクラスを自分で作成することもできます.
実は私たちが使っているsynchronizedキーワードはmonitorを使ってロック解除を実現しているので、内部ロックとも呼ばれています.Objectクラスはモニタを実現しているので、オブジェクトはいずれのオブジェクトにも内蔵されています.これがsynchronized(locker)を使う理由です.コードブロックをロックするには、lockerオブジェクトに内蔵されているmonitorを使用しただけです.各オブジェクトのmonitorクラスは一意なので、唯一のライセンスです.ライセンスを取得したスレッドは実行できます.実行後、オブジェクトを解放したmonitorは他のスレッドで取得できます.
例を挙げます.

synchronized (this) {
  //do some synchronized operation
}

バイトコードファイルでは、次のようにコンパイルされます.

monitorenter;//get monitor,enter the synchronized block
      //do some synchronized operation
monitorexit;//leavel the synchronized block,release the monitor

デッドロック
スレッドは原子性を保証することができますが、ロックと条件はマルチスレッドのすべての問題を解決することはできません.例を挙げます.
口座1残高:200
口座2残高:300
スレッド1:アカウント1→アカウント2(300)
スレッド2:アカウント2→アカウント1(400)
スレッド1とスレッド2の金額が振り替えに足りないため、両方のスレッドがブロックされている状態をデッドロック(deadlock)と呼び、すべてのスレッドがデッドロックするとプログラムが詰まってしまう.
なぜsignalAllとnotifyAll方式を使用する傾向があるのか、signalとnotifyを使用すると仮定すると、
ロックテストとタイムアウト
スレッドはlockメソッドを呼び出して別のスレッドが持つロックを取得すると、ブロックが発生する可能性が高い.より慎重にロックを申請すべきであり、tryLockメソッドはロックを申請しようとし、申請が成功すればtrueに戻り、そうでなければfalseにすぐに戻り、スレッドはブロックされてロック対象を待つのではなく、別のことをする.
構文:

ReentrantLock locker = new ReentrantLock();
if (locker.tryLock()) {
  try {
    //do some work
  } catch (Exception ex) {
    ex.printStackTrace();
  } finally {
    locker.unlock();
  }
} else {
  //do other work
}

タイムアウトパラメータを指定するもよく、単位はSECONDS、MILLISECONDS、MICROSEONDS、MANOSECONDSである.

ReentrantLock locker = new ReentrantLock();
if (locker.tryLock(1000, TimeUnit.MILLISECONDS)) {
  try {
    //do some work
  } catch (Exception ex) {
    ex.printStackTrace();
  } finally {
    locker.unlock();
  }
} else {
  //do other work
}

lockメソッドは中断できません.1つのスレッドがlockメソッドを呼び出した後にロックを待つ間に中断されると、中断スレッドはロックを取得するまでブロックされています.
タイムアウトパラメータを持つtryLockメソッドでは、待機中にスレッドが中断されるとInterruptedException例外が放出されます.これはプログラムがデッドロックを破るのに良い特性です.
リード/ライトロック
ReentrantLockクラスはjava.util.concurrent.locksパッケージに属し、このパッケージの下にはReentrantReader WriterLockクラスがあり、マルチスレッドを使用してデータを読む操作が多いが、書く操作が少ない場合はこのクラスを使用することができる.

private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock():

public void Read() {
  Lock readLocker = rwl.readLock();//       
  readLocker.lock();//         
  try {
    //do some work
  } catch (Exception ex) {
    ex.printStackTrace();
  } finally {
    readLocker.unlock();
  }
}

public void Write() {
  Lock writeLocker = rwl.writeLock();//       
  writeLocker.lock();//         
  try {
    //do some work
  } catch (Exception ex) {
    ex.printStackTrace();
  } finally {
    writeLocker.unlock();
  }
}