JAva concurrency in practice読書ノート---第三章

6695 ワード

  • 可視性
  • マルチスレッド環境ではなぜ可視性の問題があるのでしょうか.現代のCPUでは、読み書き速度の速いキャッシュをメモリとCPUとの間のバッファとして用いることが一般的である、キャッシュの導入はCPUとメモリの速度の矛盾を効果的に解決することができるが、キャッシュの一貫性という新しい問題ももたらす.マルチCPUのシステムでは、各プロセッサに独自のキャッシュがある一方、キャッシュは同一のメモリを共有するため、キャッシュ整合性の問題を解決するために、各プロセッサがキャッシュにアクセスする際に一定のプロトコルに従う必要がある.また、より良い実行効率を得るために、プロセッサはコードを乱順に最適化することができ、プロセッサは計算後に乱順実行の結果を再編成し、その結果が順序実行の結果と一致することを保証するが、プログラム中の各文の計算の順序が入力コードの順序と一致することを保証する.JAva仮想マシンは、インスタントコンパイラにおいても同様の命令再ソート最適化がある.JAvaメモリモデルは、すべての変数がメインメモリに格納ことを規定する、それ以外に各スレッドには独自のワークメモリがあり、スレッドのワークメモリにはそのスレッドで使用する変数のコピーコピーが保存され、スレッドによる変数のすべての操作(読み取り、付与など)はワークメモリで行わなければならず、メインメモリの変数を直接読み書きすることはできない.異なるスレッド間でも相手のワークメモリの変数に直接アクセスすることはできず、スレッド間の変数値の伝達はいずれもメインメモリで行う必要がある.以上から分かるように、一方のスレッドは変数の値を修正する、他方のスレッドは常に最新の値をタイムリーに知ることができるわけではない、これが可視性の問題の根源である.次のコードに従います.
    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.可変の非スレッドセキュリティのメンバーを開示.これにより、そのメンバーにアクセスするすべてのスレッドが特定のロックで同期する必要がある.