Upload
appresso-engineering-team
View
197
Download
1
Embed Size (px)
Citation preview
Effective Java 輪読会第8回(項目66~68)
2014/3/5
開発部野口
項目66
共有された可変データへのアクセスを同期する
synchronized
以下の 2 つを保証する
スレッドが、不整合な状態のオブジェクトを見ないこと
スレッドから、同じロックで保護されていた以前のすべての変更の結果が見えること
アトミックな型
long 型と double 型以外の変数の読み書きがアトミックであることは、言語仕様によって保証されている
では、パフォーマンスのため、アトミックなデータへの読み書きでは同期を避けるべき?
そうではない
メモリモデル
言語仕様では、フィールドを読み出すときにスレッドがランダムな値を読み出さないことを保証している
が、1 つのスレッドが書き込んだ値が他のスレッドからも見えることは保証していない
ので、アトミックなデータに対しても同期は有用
不完全な同期の例
public class StopThread {
private static boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(new Runnable() { public void
run() {
int i = 0;
while (!stopRequested)
i++;
}});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true; // この変更が backgroundThreadには伝わらない!
// ので、プログラムが終了しない(活性エラー)}}
巻き上げ
HotSpot Server VM は、以下のような変更を行う(この変更は許容されている)
↓
while (!stopRequested)
i++;
if (!stopRequested)
while (true) // !!!i++;
不完全な同期の修正案
public class StopThread {
private static boolean stopRequested;
public static synchronized void requestStop() {
stopRequested = true;
}
public static synchronized boolean stopRequested() {
return stopRequested;
}
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(new Runnable() { public void run() {
int i = 0;
while (!stopRequested()) i++;
}});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
requestStop(); // 変更が backgroundThreadにも反映される// ので、プログラムは 1 秒で終了する
}}
修正案の注意点
読み込み操作(stopRequested)と書き込み操作(requestStop)の両方が同期されている必要がある
volatile による修正案
public class StopThread {
private static volatile boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(new Runnable() {
public void run() {
int i = 0;
while (!stopRequested)
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true; // 反映される}
volatile の失敗例
// 不完全 -同期が必要!private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
return nextSerialNumber++; // この操作はアトミックではない!// ので、タイミング次第では、複数のスレッドで// 同じシリアルナンバーを得てしまう(安全性エ
ラー)}
AtomicLong
↑同等!(かつ、後者の方が速い可能性が高い)↓
public static synchronized long generateSerialNumber() {
if (nextSerialNumber == Long.MAX_VALUE)
throw new なんとかException();
return nextSerialNumber++;
}
private static final AtomicLong nextSerialNum = new AtomicLong();
public static long generateSerialNumber() {
return nextSerialNum.getAndIncrement();
}
可変データを共有しない
最善!
その際は、文書化しよう(方針が維持されるように)
フレームワークやライブラリを深く理解しよう(内部でスレッドが用いられているかもしれない)
事実上不変
「少しの間スレッドがデータオブジェクトを変更してから、オブジェクト参照を共有する処理だけを同期する」
「オブジェクトが再び変更されない限り、他のスレッドはさらに同期することなくオブジェクトを読み出すことができる」
// (”Java Concurrency in Practice” より引用)public Map<String, Date> lastLogin =
Collections.synchronizedMap(new HashMap<String, Date>());
安全な公開
フィールドを static にして、クラス初期化によろしくお願いする
フィールドを volatile にする
フィールドを final にする
フィールドをロックで保護する
コンカレントコレクションを用いる
まとめ
複数のスレッドが可変データを共有する場合、そのデータを読み書きするスレッドは同期を行う同期なしでは、あるスレッドの変更が他のスレッドから見えることは保証されない
同期しないことへのペナルティ:活性エラーと安全性エラーデバッグ困難!
タイミング依存、JVM 依存
スレッド間通信だけが必要な場合は、volatile もあるただし、正しく使用するのは難しい
詳しくは『Java Concurrency in Practice』を読もう(邦訳もあるよ)
項目67 過剰な同期は避ける
同期されたメソッドやブロック内で、制御をクライアントに譲らない オーバーライドされるように設計されているメソッドや関数オブジェクトを、同期された領域内で呼び出さない
<異質>なメソッドだから
メソッドが何をするかわからないため、例外、デッドロック、データ破壊の可能性がある
異質なメソッド呼び出し(例外編)
ObservableSet(pp.256-257)と、SetObserver(pp.257)
SetObserver#addedでObservableSet#removeObserverを呼び出すと、ConcurrentModificationExceptionがスローされる!(pp.257-258)
リストをイテレート中に、そのリストから要素を削除しようとしているから
異質なメソッド呼び出し(デッドロック編)
再び ObservableSet(pp.256-257)と、SetObserver(pp.257)
SetObserver#addedで、ExecutorService経由でObservableSet#removeObserverを呼び出すと、デッドロックが発生する!(pp.258)
ObservableSet.observersのロックは、既にメインスレッドによって獲得されているから
異質なメソッド呼び出し(地獄編)
もし、1. 同期された領域により保護されている不変式が一時的に不正になっている間に、
2. 同期された領域内から異質なメソッドを呼び出したら、
3. その異質なメソッドは首尾よくロックを獲得し(再帰的ロック)、
4. オブジェクトの内部状態をこっそり不正にしてしまう!(かもしれない)
ロックが用をなしていない再帰的ロックは、活性エラーを安全性エラーに変える可能性がある
異質なメソッド呼び出しの回避(オープンコール)
SetObserver#addedの処理時間が長い場合、並行性を増大させるメリットも期待できる
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);
}
異質なメソッド呼び出しの回避(CopyOnWriteArrayList)
private final List<SetObserver<E>> observers =
new CopyOnWriteArrayList<SetObserver<E>>();
public void addObserver(SetObserver<E> observer) {
observers.add(observer);
}
public void notifyElementAdded(E element) {
observer.added(this, element);
}
private void addObserver(SetObserver<E> observer) {
for (SetObserver<E> observer : observers)
observers.add(observer);
}
同期のコスト
ロックに費やされる CPU 時間
よりも、並行処理を行う機会損失
および、すべてのコアがメモリの一貫したビューを持つことを保証する必要性から生じる遅延
内部同期と外部同期
クラスが並行して使用されるのであれば、可変クラスをスレッドセーフにすべき
static フィールドへのアクセスは、必ず内部同期が必要
関係のないクライアント同士が、同じルールで同期できるとは限らないから
StringBufferと StringBuilder
StringBufferは、ほとんどの場合に単一スレッドから使用されるのに、内部的に同期を行っている
リリース 1.5 で、同期されていない StringBuilderによって置き換えられた
教訓:必要性が疑わしい場合には、内部的な同期を行わず、スレッドセーフでないことを文書化する
まとめ
デッドロックやデータ破壊を回避するため、同期された領域から異質なメソッドを呼び出さない(決して!)
より一般的には:同期された領域内で行う処理の量を制限する
可変クラスを設計する場合は、内部同期を検討する
並行性のため、過剰には同期せず、内部同期の採用が妥当でない場合はその旨をドキュメント化する
詳しくは『Java Concurrency in Practice』を読もう(邦訳もあるよ)
項目68
スレッドよりエグゼキューターとタスクを選ぶ
エグゼキューターフレームワークに親しもう
楽ちん便利
// ワークキューを作成ExecutorService executor =
Executors.newSingleThreadExecutor();
// runnable を実行のために発行executor.execute(runnable);
// 終了を指示executor.shutdown();
エグゼキューターサービスの多彩な機能
特定のタスクが完了するのを待つ
タスクの集まりの中のどれかのタスクや、すべてのタスクが完了するのを待つ
エグゼキューターサービスがきちんと完了するのを待つ
タスクが完了するごとに、1 つずつタスクの結果を取り出す
スレッドプールを作成する
エグゼキューターサービスの選択
小さなプログラムや、軽い負荷のサーバーなら:
Executors.newCachedThreadPool
設定不要、一般に「正しいことを行う」
高負荷の製品サーバーなら:
Executors.newFixedThreadPool
固定数のスレッドを持つプールを提供する
ThreadPoolExecutorを直接使用
最大限の制御が可能
Thread を直接使うのはもうやめよう
Thread
処理の単位と、処理を実行するための機構の両方
Runnable / Callable / エグゼキューターサービス
<タスク>(処理の単位)とその実行機構を、それぞれ適切に抽象化する
Timer を直接使うのももうやめよう
Timer
タスク実行のために単一スレッドしか使用していない
タイミングの精度に不安あり
例外がスローされると、動作しなくなる
ScheduledThreadPoolExecutor
複数スレッドをサポート
チェックされない例外をスローする例外からも回復
まとめ
詳しくは『Java Concurrency in Practice』を読もう(邦訳もあるよ)