Effective Java学習(同時)の-同期の過度な使用を避ける

10029 ワード

同期は、実際の開発に多くのメリットをもたらし、同期を合理的に使用することで、マルチスレッドと同時実行時のデータの共有と一貫性をよりよく処理できます.しかし、1つの状況によっては、同期を過度に使用すると、パフォーマンスが低下したり、デッドロックしたり、不確定な動作をしたりする可能性があります.
 
アクティブな失敗とセキュリティの失敗を回避するために、同期された方法またはコードブロックでは、クライアントの制御を決して放棄しないでください.すなわち、同期された領域の内部では、上書きされるように設計された方法、またはクライアントによって関数として提供される方法を呼び出さないでください.この同期領域を含むクラスの観点から,このような方法は時々外来的である.この類はこの方法が何をするか分からないし、コントロールできない.外部メソッドの役割に応じて、同期領域から呼び出すと、異常、デッドロック、またはデータ破損が発生します.
 
この過程を具体的に説明するために,以下のクラスを考慮すると,観察可能な集合包装を実現した.このクラスでは、クライアントが要素をセットに追加するときに通知を予約できます.これが観察者モードです.簡潔化のため、クラスは要素をコレクションから削除するときに通知を提供しませんが、通知を提供するのも簡単です.
 
import java.util.*;


public class ForwardingSet<E> implements Set<E>{
	private final Set<E> s;
	
	public ForwardingSet(Set<E> s){
		this.s = s;
	}

	@Override
	public int size() {return this.s.size();}
	
	@Override
	public void clear() {this.s.clear();}
	
	@Override
	public boolean isEmpty() {return this.s.isEmpty();}

	@Override
	public boolean contains(Object o) {return this.s.contains(o);}

	@Override
	public Iterator<E> iterator() {return this.s.iterator();}

	@Override
	public Object[] toArray() {return this.s.toArray();}

	@Override
	public <T> T[] toArray(T[] a) {return this.s.toArray(a);}

	@Override
	public boolean add(E e) {return this.s.add(e);}

	@Override
	public boolean remove(Object o) {return this.s.remove(o);}

	@Override
	public boolean containsAll(Collection<?> c) {return this.s.containsAll(c);}

	@Override
	public boolean addAll(Collection<? extends E> c) {return this.s.addAll(c);}

	@Override
	public boolean retainAll(Collection<?> c) {return this.s.removeAll(c);}

	@Override
	public boolean removeAll(Collection<?> c) {return this.s.retainAll(c);}
} 

 
 
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;

public class ObservableSet<E> extends ForwardingSet<E>{

	public ObservableSet(Set<E> s) {
		super(s);
	}
	
	
	private final List<SetObserver<E>>  observers = 
		new ArrayList<SetObserver<E>>();
	
	public void addObserver(SetObserver<E> observer){
		synchronized (observers) {
			observers.add(observer);
		}
	}
	
	public boolean removeObserver(SetObserver<E> observer){
		synchronized (observers) {
			return observers.remove(observer);
		}
	}
	
	private void notifyElementAdded(E element){
		synchronized (observers) {
			for(SetObserver<E> observer : observers){
				observer.added(this, element);
			}
		}
	}

	@Override
	public boolean add(E element) {
		boolean added = super.add(element);
		if (added) {
			notifyElementAdded(element);
		}
		return added;
	}

	@Override
	public boolean addAll(Collection<? extends E> c) {
		boolean result = false;
		for(E element : c){
			result |= add(element);
		}
		return result;
	}
}

ObserverはaddObserverメソッドで通知を予定し,removeObserveメソッドを呼び出すことで予定をキャンセルする.どちらの場合も、このコールバックインタフェースのインスタンスはメソッドに渡されます.
 
public interface SetObserver<E> {
	void added(ObservableSet<E> set, E element);
}

大雑把にチェックすればObserverSetは正常に見えます.たとえば、次のプログラムでは0~99の数字を印刷します.
 
 
public static void main(String[] args) {
		ObservableSet<Integer> set = new ObservableSet<Integer>(new HashSet<Integer>());
		set.addObserver(new SetObserver<Integer>() {
			
			@Override
			public void added(ObservableSet<Integer> set, Integer element) {
				System.out.print(element+" ");
			}
		});
		
		for(int i = 0; i < 100; i++){
			set.add(i);
		}
	}

もっと複雑な例を試してみましょう.この呼び出しの代わりにaddObserver呼び出しを使用すると、置換用のaddObserver呼び出しは印刷Integerのオブザーバーを渡し、この値がセットに追加され、値が23の場合、このオブザーバーは自分自身を削除します.
 
 
set.addObserver(new SetObserver<Integer>() {
			
			@Override
			public void added(ObservableSet<Integer> set, Integer element) {
				System.out.print(element+" ");
				if (element == 23) {
					set.removeObserver(this);
				}
			}
		});

このプログラムは0-23の数字を印刷し、観察者が予約をキャンセルし、プログラムがこっそり仕事を完成すると思っているかもしれません.実際に0-23の数字を印刷して異常なC o n c u r r e ntModificationExceptionを放出します.問題は、notifyElementAddedがオブザーバーのaddedメソッドを呼び出すと、objserversリストを巡る過程にあることです.addedメソッドは、観察可能な集合のremoveObserverメソッドを呼び出し、observersを呼び出す.removeメソッド.今、私たちは困っています.リストを巡回する過程で、1つの要素をリストから削除しようとしています.これは不正です.notifyElementAddedメソッドの反復式は同期ブロックで、同時修正を防止できますが、反復スレッド自体が観察可能なセットにコールバックしたり、observersリストを変更したりすることは防止できません.
 
 
ここでは、予定をキャンセルしようとするオブザーバーを作成しますが、removeObserverを直接呼び出さず、別のスレッドのサービスで完了します.このオブザーバーはexecutorサービスを使用しています.
 
set.addObserver(new SetObserver<Integer>() {
			
			@Override
			public void added(final ObservableSet<Integer> set, Integer element) {
				System.out.print(element+" ");
				if (element == 23) {
					ExecutorService executorService = Executors.newSingleThreadExecutor();
					
					final SetObserver<Integer> observer = this;
					try {
						executorService.submit(new Runnable() {
							@Override
							public void run() {
								set.removeObserver(observer);
							}
						}).get();
					} catch (ExecutionException ex) {
						throw new AssertionError(ex.getCause());
					}catch (InterruptedException ex) {
						throw new AssertionError(ex.getCause());
					}finally{
						executorService.shutdown();
					}
				}
			}
		});

今回私たちは異常に遭遇するのではなく、デッドロックに遭った.バックグラウンドスレッド呼び出しset.removeObserverは、observersをロックしようとしたが、プライマリスレッドにロックがないため、ロックを取得できなかった.その間、プライマリ・スレッドはバックグラウンド・プログラムを待って観察者の削除を完了し、デッドロックの原因となります.
 
 
この例は,観察者が実際にバックグラウンドスレッドを使用する理由がないため,モデルを記述することができるが,この問題は真実である.同期領域からの外来法は,GUIツールボックスのような実際のシステムに多くのデッドロックをもたらした.
 
前の2つの例(異常とデッドロック)では、私たちはまだラッキーです.外来メソッドを呼び出すと、同期領域で保護されているリソースは一貫した状態になります.アーカイブ同期領域で保護されている制約が一時的に無効であると仮定すると、同期領域から外部メソッドを呼び出します.Javaプログラム設計言語のロックは再入力可能であるため、この呼び出しはデッドロックしません.最初の例と同様に、呼び出しスレッドにはすでにこのロックがあるため、スレッドがロックを再取得しようとすると、概念的に関連しない別の操作がロックによって保護されたデータ上で行われているにもかかわらず、例外が発生します.このような失敗の結果は災難的かもしれない.本質的には、この鍵は彼の職責を果たしていない.再人間ロックは、マルチスレッドのオブジェクト向けプログラムの構造を簡略化するが、hiは活性の失敗をセキュリティの失敗に変える可能性がある.
 
幸いなことに、この問題を解決するには、外部メソッドの呼び出しを同期コードブロックから削除することは通常あまり難しくありません.notofyElementAddedメソッドでは、これはobserversリストに「スナップショット」を撮るように設計されており、ロックがなくても安全にこのリストを巡回することができます.この変更を加えると、前の2つの例が実行されても異常やデッドロックは発生しません.
 
private void notifyElementAdded(E element) {
		List<SetObserver<E>> snapshot = null;
		
		synchronized (observers) {
			snapshot = new ArrayList<SetObserver<E>>(observers);
		}
		
		for(SetObserver<E> observer : snapshot){
			observer.added(this, element);
		}
	}

実際には、外部のメソッド呼び出しを同期コードブロックから削除するには、より良い方法があります.Java 1から5以降、javaクラスライブラリには、このためにカスタマイズされたCopyOnWriteArrayListと呼ばれる同時集合が提供されます.これはArrayListの一種の変種であり,ここでは下位配列全体を再コピーすることによってすべての書き込み動作を実現する.内部配列は永遠に変更されないため,反復はロックを必要とせず,速度も非常に速い.大量に使用すると、CopyOnWriteArrayListのパフォーマンスは大きく影響しますが、ほとんど変更されず、頻繁に遍歴しているため、観察者リストにとっては良いです.
 
 
このリストをCopyOnWriteArrayListに変更すると、ObservableSetのaddメソッドとaddAllメソッドを変更する必要はありません.次はこのクラスの残りのコードです.表示される同期はありません.
 
private final List<SetObserver<E>> observers = new CopyOnWriteArrayList<SetObserver<E>>();
	
	public void addObserver(SetObserver<E> observer) {
		observers.add(observer);
	}

	public boolean removeObserver(SetObserver<E> observer) {
		return observers.remove(observer);
	}

	private void notifyElementAdded(E element) {
		for (SetObserver<E> observer : observers) {
				observer.added(this, element);
		}
}

同期領域の外で呼び出される外来メソッドは、「オープンコール」と呼ばれます.デッドロックを回避できるほか、オープンコールは同時性を大幅に増加させることができます.外来メソッドの実行時間は任意に長くなる可能性があります.同期領域で外部メソッドを呼び出すと、保護されたリソースへの他のスレッドのアクセスが不要に拒否されます.
 
 
通常、同期領域でできるだけ少ない仕事をしなければなりません.ロックを取得し、共有データをチェックし、必要に応じてデータを変換し、ロックを解除します.時間のかかる動作を実行する必要がある場合は、同期領域の外に移動するように工夫する必要があります.
 
本編の第1部は正確性についてです.次に、パフォーマンスについて簡潔に説明します.Javaプラットフォームの初期以来、同期のコストは低下していますが、さらに重要なのは、過度な同期は決して行わないことです.このマルチコアの時代、過度な同期の実際のコストは、デッドロックを取得するのにかかるCPU時間ではなく、並列の機会を失ったこと、および各コアが一貫したメモリビューを持っていることを確保する必要があるための遅延です.過剰同期のもう一つの潜在的なオーバーヘッドは、VM最適化コードの実行能力を制限することです.
 
可変クラスが同時に使用される場合は、このクラスのプログラミングスレッドを安全にする必要があります.内部同期により、外部からオブジェクト全体をロックするよりも明らかに高い同時性を得ることができます.そうでなければ、内部で同期しないでください.必要に応じて外部から同期させる.Javaプラットフォームが登場した初期には、多くのクラスがこのガイドラインに違反していました.たとえば、StringBufferインスタンスはほとんど単一スレッドで使用されますが、内部同期が実行されます.そのため、StringBufferは基本的にStringBuilderの代わりにjava 1にいます.5では非同期のStringBufferです.不確実な場合は、クラスを同期するのではなく、スレッドが安全ではないことを示すドキュメントを作成する必要があります.
 
内部でクラスを同期すると、分割ロック、分離ロック、非ロック同時制御など、異なる方法で高同時性を実現できます.
 
もし方法が静的ドメインを修正したら、私もこのドメインのアクセスを同期しなければなりません.たとえ彼が単一のスレッドにしか使用されないとしても.お客様は、この方法で外部同期を実行することは不可能です.他の関連のないお客様も外部同期を実行することを保証することはできません.
 
簡単に言えば、デッドロックやデータ破壊を避けるために、同期領域から外部のメソッドを呼び出さないでください.より一般的には、同期領域内のワークロードをできるだけ制限します.可変クラスを設計するときは、同期操作を自分で完了すべきかどうかを考えてみましょう.今のマルチコアの時代、これは永遠にマルチ同期しないよりも重要で、内部でクラスを同期しなければならない十分な理由があるときだけ、このようにすることができます.同時に、この決定をドキュメントに明確に書く必要があります.