Race condition--Java Concurrency In Practice C 02読書ノート
[本文はJava Concurrency In Practiceの第2章のまとめとまとめです. 転載は作者と出典を明記してください. 誤りがあれば、コメントで訂正してください. ]
マルチスレッド環境では、呼び出し元が同期処理を行わなくても正確性が保証されるクラスは、スレッドの安全なクラスです.
ステータスのないオブジェクトはスレッドが安全です.ステータスのないオブジェクトは、メンバー変数がありません.メソッドのローカル変数は、スレッドのプライベートスタックに割り当てられているため、1つのスレッドでステータスのないオブジェクトを呼び出すメソッドは、他のスレッドには影響しません.
race condition:正確性はイベント発生の相対時間に依存する.
check-and-actはrace conditionの一種で、checkの結果に基づいて動作することを指す.checkとactは原子ではないため、actを行う際にcheckの結果が無効になっている可能性があり、checkに基づくactは問題をもたらす可能性がある.
次のlazy単一クラスを参照してください.
Read‐modify‐writeもrace conditionの一種であり、ある変数の値を読み取り、修正して書き返すことを指す.これは明らかに原子操作ではなく、BスレッドがAスレッドreadの後writeの前に変数の値を修正すると、Aスレッドreadの結果が失効し、readに基づくmodifyが問題をもたらす可能性がある.
次のservletを参照してください.
Javaが提供する同期機構を用いてcheck-and-actまたはRead‐modify‐writeを原子操作に変換するとrace conditionによるエラーを回避できる.
同期メカニズムを使用してLazyInitRaceとCountingFactorizerクラスを改善し、スレッドの安全なクラスにします.
メンバー変数が1つしかないオブジェクトの場合、メンバーの状態はオブジェクトの状態です.メンバーの操作がすべて原子である場合、クラスはスレッドが安全なクラスです.たとえば、メンバー変数instanceが1つしかなく、instanceの操作が原子であるLazyInitRaceクラスは、スレッドが安全なクラスです.
クラスの各メンバー変数の操作が原子である限り、クラスはスレッドで安全ですか?いいえ.
各メンバーの操作はすべて原子であるため、メンバーに関連するすべての操作が全体的に原子であることは保証されません.たとえば、次のようになります.
JAvaのsynchronizedメカニズムで使用されるロックは、再ロック可能である.つまり、同じスレッドがデッドロックを起こさずに同じロックを複数回申請することができる.Aスレッドがlockを持っていると仮定すると、Bスレッドがlockロックを申請すると、Bスレッドはブロックされる.しかし、Aスレッドが保有しているlockロックを再申請すると、その申請は通過する.これは、いわゆる同じスレッドが複数回取得できることである同じロックです.ロックの対象として、ロックの所有者を識別するだけでなく、所有者が持っているカウントも識別する必要があります.所有者スレッドがロックを申請したばかりの場合、カウンタの値は1で、再取得するたびに、カウンタの値に1を加え、同期コードブロックを終了するたびに、カウンタの値は1に減少します.カウンタの値が0に減少すると、所有者スレッドはロックを解放します.ロックの設計は、申請したロックによってデッドロックが発生することを防止するために設計されています.例えば、次のようなものです.
マルチスレッド環境では、呼び出し元が同期処理を行わなくても正確性が保証されるクラスは、スレッドの安全なクラスです.
ステータスのないオブジェクトはスレッドが安全です.ステータスのないオブジェクトは、メンバー変数がありません.メソッドのローカル変数は、スレッドのプライベートスタックに割り当てられているため、1つのスレッドでステータスのないオブジェクトを呼び出すメソッドは、他のスレッドには影響しません.
race condition:正確性はイベント発生の相対時間に依存する.
check-and-actはrace conditionの一種で、checkの結果に基づいて動作することを指す.checkとactは原子ではないため、actを行う際にcheckの結果が無効になっている可能性があり、checkに基づくactは問題をもたらす可能性がある.
次のlazy単一クラスを参照してください.
public class LazyInitRace {
private static ExpensiveObject instance = null;
public static ExpensiveObject getInstance() {
// if check-and-act
if (instance == null) {
instance = new ExpensiveObject();
}
return instance;
}
}
これはcheck-and-actの典型的な例です.まずinstanceがnullであるかどうかを判断し、ExpensiveObjectオブジェクトを作成した場合、そのままinstanceに戻ります.しかし、判断と作成は原子操作ではなく、スレッド1がinstanceがnullであると判断し、別のスレッドがExpensiveObjectオブジェクトを作成した場合、スレッド1の判断は失効し、判断結果に基づいて作成操作を行うと、プログラムに複数のExpensiveObjectオブジェクトが存在します.これは、単一のモードの目的に反します.Read‐modify‐writeもrace conditionの一種であり、ある変数の値を読み取り、修正して書き返すことを指す.これは明らかに原子操作ではなく、BスレッドがAスレッドreadの後writeの前に変数の値を修正すると、Aスレッドreadの結果が失効し、readに基づくmodifyが問題をもたらす可能性がある.
次のservletを参照してください.
public class CountingFactorizer implements Servlet {
private final long count = 0;
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
// // Read‐modify‐write
count++;
encodeIntoResponse(resp, factors);
}
}
Javaが提供する同期機構を用いてcheck-and-actまたはRead‐modify‐writeを原子操作に変換するとrace conditionによるエラーを回避できる.
同期メカニズムを使用してLazyInitRaceとCountingFactorizerクラスを改善し、スレッドの安全なクラスにします.
public class LazyInitRace {
private static ExpensiveObject instance = null;
public static ExpensiveObject getInstance() {
// synchronized check-and-act
if (instance == null) {
synchronized (LazyInitRace.class) {
if (instance == null) {
instance = new ExpensiveObject();
}
}
}
return instance;
}
}
public class CountingFactorizer implements Servlet {
private final AtomicLong count = new AtomicLong(0);
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
// long AtomicLong, Read‐modify‐write
count.incrementAndGet();
encodeIntoResponse(resp, factors);
}
}
メンバー変数が1つしかないオブジェクトの場合、メンバーの状態はオブジェクトの状態です.メンバーの操作がすべて原子である場合、クラスはスレッドが安全なクラスです.たとえば、メンバー変数instanceが1つしかなく、instanceの操作が原子であるLazyInitRaceクラスは、スレッドが安全なクラスです.
クラスの各メンバー変数の操作が原子である限り、クラスはスレッドで安全ですか?いいえ.
各メンバーの操作はすべて原子であるため、メンバーに関連するすべての操作が全体的に原子であることは保証されません.たとえば、次のようになります.
public class UnsafeCachingFactorizer implements Servlet {
private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
//
if (i.equals(lastNumber.get()))
encodeIntoResponse(resp, lastFactors.get() );
else {
BigInteger[] factors = factor(i);
//
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, factors);
}
}
}
UnsafeCachingFactorizerクラスの2つのメンバーlastNumberとlastFactorsのset()とget()メソッドは原子的であるが、このクラスはスレッドが安全ではない.全体的に2回の書き込みと2回の読み取りが同時に行われないため、UnsafeCachingFactorizerクラスはrace conditionに残っている.JAvaのsynchronizedメカニズムで使用されるロックは、再ロック可能である.つまり、同じスレッドがデッドロックを起こさずに同じロックを複数回申請することができる.Aスレッドがlockを持っていると仮定すると、Bスレッドがlockロックを申請すると、Bスレッドはブロックされる.しかし、Aスレッドが保有しているlockロックを再申請すると、その申請は通過する.これは、いわゆる同じスレッドが複数回取得できることである同じロックです.ロックの対象として、ロックの所有者を識別するだけでなく、所有者が持っているカウントも識別する必要があります.所有者スレッドがロックを申請したばかりの場合、カウンタの値は1で、再取得するたびに、カウンタの値に1を加え、同期コードブロックを終了するたびに、カウンタの値は1に減少します.カウンタの値が0に減少すると、所有者スレッドはロックを解放します.ロックの設計は、申請したロックによってデッドロックが発生することを防止するために設計されています.例えば、次のようなものです.
public class Widget {
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
public synchronized void doSomething() {
System.out.println(toString() + ": calling doSomething");
super.doSomething();
}
}
Javaのロックが再読み込み可能でない場合、LoggingWidgetオブジェクトのdoSomethingメソッドを呼び出すとデッドロックになります.