JAva concurrency in practice読書ノート---第三章
6695 ワード
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
public void run() {
while (!ready)
Thread.yield();
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
NoVisibilityはループを続ける可能性があります.リードスレッドはreadyの直さが永遠に見えない可能性があります.さらに奇妙な現象は、読み取りスレッドがreadyに書き込まれた値を見たかもしれないが、numberに書き込まれた値を見たことがないため、NoVisibilityが0を出力する可能性があることです.
synchronizedと可視性
synchronizedキーワードは操作の原子性を保証するだけでなく、変数の可視性も保証できる.JVM仕様では、スレッドAとスレッドBが同一のロックにより同期場合、スレッドAが同期コードブロックにおいて行った変更はスレッドBに対して可視とする.
volatileと可視性
JAvaの同期メカニズムにはsynchronizedのほかにvolatileがある.
volatileキーワードを使用して変数を修飾すると、その変数は「可変」と宣言される.JVM仕様では、どのスレッドでもvolatile変数の値を変更するには、直ちに新しい値をメインメモリに更新する必要があり、どのスレッドでもvolatile変数を使用する場合は、メインメモリの変数値を再取得する必要があり、volatileキーワードには、命令再ソート最適化を禁止する意味が隠されている.以上の仕様はvolatile変数のスレッド可視性を保証する.
volatileは軽量級の同期機構であり、synchronizedと異なり、volatileは操作の原子性を保証できず、変数の可視性しか保証できない.したがって、volatileキーワードの使用は厳格に制限されており、volatileキーワードの正しい使用は以下の条件を同時に満たす必要があります.
1.変更は現在の値に依存する、または単一スレッドでのみ変数の値を変更できることを保証する.変数の変更が既存の値に依存するrace condition操作である場合、synchronizedのような他の同期手段を用いてrace condition操作を原子操作に変換する必要があるが、volatileは原子性には力がない.ただし、単一スレッドでのみ変数の値が変更することが確実であれば、現在のスレッドを除く他のスレッドでは変数の値を変更することができず、race conditionが発生することはない.
2.変数は他の状態変数と共に不変拘束に関与する必要はない.
64ビットデータ(longとdoubleタイプ)
JVM仕様では、仮想マシンがlongとdouble型の非volatileデータの読み書き操作を2回32ビットの操作に分割する行うことができる.複数のスレッドが1つの非volatileのlongまたはdouble変数を共有する、同時にその変数を読み取りおよび修正する場合、一部のスレッドは、元の値でも他のスレッドの修正値でもない「半変数」を表す数値を読み取る可能性がある.
幸いなことに、ほとんどのプラットフォームの商用仮想マシンは64をデータとする読み書き操作を原子操作として扱うことをほとんど選択しています.そうしないと、javaプログラマーはlongとdouble変数を使用する際にvolatileとして変数を宣言する必要があります.
スレッドのクローズ
オブジェクトのアクセスは、約定やjava内蔵のThreadLocalによって単一のスレッドに制限ことができる、これにより、オブジェクトがスレッドでなくても安全である、エラーが発生しない.
例えばandroidのGUUIフレームワークは、すべてのコントロールの更新がメインスレッドで発生する必要があることを規定しているので、androidのViewコンポーネントがスレッドの安全なオブジェクトではないとしても、同時エラーを引き起こす心配はありません.開発者がandroidコントロールオブジェクトのスレッド制限に従わない場合、プログラム実行時に異常が放出される.
スレッド制限のもう一つの利点は、デッドロックを防止できることである.
ThreadLocalはスレッド内でオブジェクトを共有するために用いることが多い.
不変クラス
すべての同時問題は、複数のスレッドが同時にオブジェクトにアクセスする可変属性に起因し、オブジェクトが可変である場合、すべての同時問題が解決される.
可変オブジェクトとは、オブジェクトの構造が完了すると、そのすべての属性が変更することができず、可変オブジェクトは明らかにスレッドが安全であることを意味する.
可変オブジェクトについては、this逃走の発生を防止する必要がある.
複数のメンバーに対して1つの原子操作を行う必要がある場合、これらのメンバーを用いて可変クラスを構築することが考えられる.例:
public class CashedClass {
private String cashedStr = "";
private int cashedHashCode;
public int hashCode(String str) {
// str cashedStr, hashCode
if (str.equals(cashedStr)) {
return cashedHashCode;
} else {
// cashedStr hashCode
cashedStr = str;
cashedHashCode = cashedStr.hashCode();
return cashedHashCode;
}
}
}
CashedClassはスレッドの安全なクラスではありません.cashedStrとcashedHashCodeの読み書き操作に原子性がないため、race conditionが発生します.synchronizedを使用して同期するほか、可変オブジェクトを使用してrace conditionを除去することもできます.
public class CashedClass {
// volatile OneCashedValue
private volatile OneCashedValue oneValue = new OneCashedValue("", 0);
public int hashCode(String str) {
int hashCode = oneValue.getStrHashCode(str);
if (hashCode == -1) {
hashCode = str.hashCode();
// volatile , volatile
oneValue = new OneCashedValue(str, hashCode);
}
return hashCode;
}
/**
*
*/
public class OneCashedValue {
// final
private final String str;
private final int strHashCode;
// this
public OneCashedValue(String str, int strHashCode) {
this.str = str;
this.strHashCode = strHashCode;
}
public int getStrHashCode(String str) {
if (!this.str.equals(str)) {
// -1 hashCode
return -1;
}
return strHashCode;
}
}
}
オブジェクトのパブリケーション
public class ThisEscape {
public ThisEscape() {
new Thread(new EscapeRunnable()).start();
// ...
}
private class EscapeRunnable implements Runnable {
@Override
public void run() {
// ThisEscape.this , , this
}
}
}
ここから逃げる
コンストラクション関数が戻る前に他のスレッドがそのオブジェクトの参照を持つことを指す.まだ完全に構成するオブジェクトを呼び出す方法は、疑わしいエラーを引き起こす可能性があるため、this逃走の発生を避けるべきである.
この脱出は、コンストラクション関数でスレッドを起動したり、リスナーを登録したりするときによく発生します.
public class ThisEscape {
public ThisEscape() {
new Thread(new EscapeRunnable()).start();
// ...
}
private class EscapeRunnable implements Runnable {
@Override
public void run() {
// ThisEscape.this , , this
}
}
}
コンストラクション関数でThreadオブジェクトを作成するのは問題ありませんが、Threadを起動しないでください.次のようなinitメソッドを提供できます.
public class ThisEscape {
private Thread t;
public ThisEscape() {
t = new Thread(new EscapeRunnable());
// ...
}
public void init() {
t.start();
}
private class EscapeRunnable implements Runnable {
@Override
public void run() {
// ThisEscape.this ,
}
}
}
公開メンバー
オブジェクトは、オブジェクト以外のコードが対応するメンバーにアクセスできるように、メソッドパラメータ、メソッド戻り値、非private修飾などの方法でオブジェクトのメンバーを公開することができる.
オブジェクトのメンバが公開すると、マルチスレッド環境で公開されるメンバの可視性を保証する必要がある、いわゆる安全な公開である.メンバー変数を開示する前提は、メンバー変数が不変制約に関与せず、メンバー変数に不正値がないことである.公開されると、外部で変数を変更した後も変数が不変制約を満たし、不正値を取らないことを保証できないからである.前提条件を満たす場合、公開されたオブジェクトのメンバーは、以下の方法で安全になります.
1.スレッド制限制限オブジェクトが単一のスレッドのみでアクセス可能であると、どのメンバーが開示も同時問題は発生しない.
2.可変メンバーを公開する.オブジェクトのあるメンバーが可変である場合、そのメンバーが同時に問題を生じることはない.
3.事実上の可変メンバーを公開する.オブジェクトのメンバーが可変であるが、そのメンバーにアクセスするすべてのスレッドがこのメンバーを変更しないことを約束する場合、そのメンバーは事実上可変である.このような場面で公開するメンバーには同時問題は生じない.
4.スレッドの安全なメンバーを公開する.スレッドセキュリティのメンバー内部では問題が適切に併発するため、公開スレッドセキュリティのメンバーが適切である.
5.可変の非スレッドセキュリティのメンバーを開示.これにより、そのメンバーにアクセスするすべてのスレッドが特定のロックで同期する必要がある.