More Related Content Similar to Effective Java 輪読会 項目66-68 (20) More from Appresso Engineering Team (15) Effective Java 輪読会 項目66-683. synchronized
以下の2 つを保証する
スレッドが、不整合な状態のオブジェクトを見な
いこと
スレッドから、同じロックで保護されていた以前
のすべての変更の結果が見えること
4. アトミックな型
long 型とdouble 型以外の変数の読み書きがア
トミックであることは、言語仕様によって保
証されている
では、パフォーマンスのため、アトミックなデー
タへの読み書きでは同期を避けるべき?
そうではない
6. 不完全な同期の例
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 には伝わらな
い!
// ので、プログラムが終了しない(活性エラー)
}}
7. 巻き上げ
HotSpot Server VM は、以下のような変更を行う(この
変更は許容されている)
↓
while (!stopRequested)
i++;
if (!stopRequested)
while (true) // !!!
i++;
8. 不完全な同期の修正案
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 秒で終了する
}}
10. 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; // 反映される
}
11. volatile の失敗例
// 不完全- 同期が必要!
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
return nextSerialNumber++; // この操作はアトミックではない!
// ので、タイミング次第では、複数のスレッドで
// 同じシリアルナンバーを得てしまう(安全性エ
ラー)
}
12. 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();
}
13. 可変データを共有しない
最善!
その際は、文書化しよう(方針が維持される
ように)
フレームワークやライブラリを深く理解しよ
う(内部でスレッドが用いられているかもし
れない)
14. 事実上不変
「少しの間スレッドがデータオブジェクトを
変更してから、オブジェクト参照を共有する
処理だけを同期する」
「オブジェクトが再び変更されない限り、他のス
レッドはさらに同期することなくオブジェクトを
読み出すことができる」
// (”Java Concurrency in Practice” より引用)
public Map<String, Date> lastLogin =
Collections.synchronizedMap(new HashMap<String, Date>());
15. 安全な公開
フィールドをstatic にして、クラス初期化に
よろしくお願いする
フィールドをvolatile にする
フィールドをfinal にする
フィールドをロックで保護する
コンカレントコレクションを用いる
16. まとめ
複数のスレッドが可変データを共有する場合、そ
のデータを読み書きするスレッドは同期を行う
同期なしでは、あるスレッドの変更が他のスレッドか
ら見えることは保証されない
同期しないことへのペナルティ:活性エラーと安
全性エラー
デバッグ困難!
タイミング依存、JVM 依存
スレッド間通信だけが必要な場合は、volatile も
ある
ただし、正しく使用するのは難しい
詳しくは『Java Concurrency in Practice』を読も
う(邦訳もあるよ)
19. 異質なメソッド呼び出し
(例外編)
ObservableSet(pp.256-257)と、
SetObserver(pp.257)
SetObserver#added で
ObservableSet#removeObserver を呼び出すと、
ConcurrentModificationException がスローされ
る!(pp.257-258)
リストをイテレート中に、そのリストから要素を削除
しようとしているから
20. 異質なメソッド呼び出し
(デッドロック編)
再びObservableSet(pp.256-257)と、
SetObserver(pp.257)
SetObserver#added で、ExecutorService 経由で
ObservableSet#removeObserver を呼び出すと、
デッドロックが発生する!(pp.258)
ObservableSet.observers のロックは、既にメインス
レッドによって獲得されているから
21. 異質なメソッド呼び出し
(地獄編)
もし、
1. 同期された領域により保護されている不変式が一時
的に不正になっている間に、
2. 同期された領域内から異質なメソッドを呼び出した
ら、
3. その異質なメソッドは首尾よくロックを獲得し(再
帰的ロック)、
4. オブジェクトの内部状態をこっそり不正にしてしま
う!(かもしれない)
ロックが用をなしていない
再帰的ロックは、活性エラーを安全性エラーに変える
可能性がある
22. 異質なメソッド呼び出しの回避
(オープンコール)
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);
}
SetObserver#added の処理時間が長い場合、並行性
を増大させるメリットも期待できる
23. 異質なメソッド呼び出しの回避
(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);
}
25. 内部同期と外部同期
クラスが並行して使用されるのであれば、可変クラス
をスレッドセーフにすべき
static フィールドへのアクセスは、必ず内部同期が必
要
関係のないクライアント同士が、同じルールで同期できる
とは限らないから
StringBuffer とStringBuilder
StringBuffer は、ほとんどの場合に単一スレッドから使用
されるのに、内部的に同期を行っている
リリース1.5 で、同期されていないStringBuilder によっ
て置き換えられた
教訓:必要性が疑わしい場合には、内部的な同期を行わず、
スレッドセーフでないことを文書化する
26. まとめ
デッドロックやデータ破壊を回避するため、同期
された領域から異質なメソッドを呼び出さない
(決して!)
より一般的には:同期された領域内で行う処理の量を
制限する
可変クラスを設計する場合は、内部同期を検討す
る
並行性のため、過剰には同期せず、内部同期の採用が
妥当でない場合はその旨をドキュメント化する
詳しくは『Java Concurrency in Practice』を読も
う(邦訳もあるよ)
28. エグゼキューターフレームワーク
に
親しもう
// ワークキューを作成
ExecutorService executor =
Executors.newSingleThreadExecutor();
// runnable を実行のために発行
executor.execute(runnable);
// 終了を指示
executor.shutdown();
楽ちん便利
29. エグゼキューターサービスの
多彩な機能
特定のタスクが完了するのを待つ
タスクの集まりの中のどれかのタスクや、す
べてのタスクが完了するのを待つ
エグゼキューターサービスがきちんと完了す
るのを待つ
タスクが完了するごとに、1 つずつタスクの
結果を取り出す
スレッドプールを作成する
30. エグゼキューターサービスの選択
小さなプログラムや、軽い負荷のサーバーな
ら:
Executors.newCachedThreadPool
設定不要、一般に「正しいことを行う」
高負荷の製品サーバーなら:
Executors.newFixedThreadPool
固定数のスレッドを持つプールを提供する
ThreadPoolExecutor を直接使用
最大限の制御が可能
31. Thread を直接使うのはもうやめよ
う
Thread
処理の単位と、処理を実行するための機構の両方
Runnable / Callable / エグゼキューターサービ
ス
<タスク>(処理の単位)とその実行機構を、そ
れぞれ適切に抽象化する
32. Timer を直接使うのももうやめよ
う
Timer
タスク実行のために単一スレッドしか使用してい
ない
タイミングの精度に不安あり
例外がスローされると、動作しなくなる
ScheduledThreadPoolExecutor
複数スレッドをサポート
チェックされない例外をスローする例外からも回
復