33
Effective Java 輪読会 第8(項目66682014/3/5 開発部 野口

Effective Java 輪読会 項目66-68

Embed Size (px)

Citation preview

Page 1: Effective Java 輪読会 項目66-68

Effective Java 輪読会第8回(項目66~68)

2014/3/5

開発部野口

Page 2: Effective Java 輪読会 項目66-68

項目66

共有された可変データへのアクセスを同期する

Page 3: Effective Java 輪読会 項目66-68

synchronized

以下の 2 つを保証する

スレッドが、不整合な状態のオブジェクトを見ないこと

スレッドから、同じロックで保護されていた以前のすべての変更の結果が見えること

Page 4: Effective Java 輪読会 項目66-68

アトミックな型

long 型と double 型以外の変数の読み書きがアトミックであることは、言語仕様によって保証されている

では、パフォーマンスのため、アトミックなデータへの読み書きでは同期を避けるべき?

そうではない

Page 5: Effective Java 輪読会 項目66-68

メモリモデル

言語仕様では、フィールドを読み出すときにスレッドがランダムな値を読み出さないことを保証している

が、1 つのスレッドが書き込んだ値が他のスレッドからも見えることは保証していない

ので、アトミックなデータに対しても同期は有用

Page 6: Effective Java 輪読会 項目66-68

不完全な同期の例

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には伝わらない!

// ので、プログラムが終了しない(活性エラー)}}

Page 7: Effective Java 輪読会 項目66-68

巻き上げ

HotSpot Server VM は、以下のような変更を行う(この変更は許容されている)

while (!stopRequested)

i++;

if (!stopRequested)

while (true) // !!!i++;

Page 8: Effective Java 輪読会 項目66-68

不完全な同期の修正案

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 秒で終了する

}}

Page 9: Effective Java 輪読会 項目66-68

修正案の注意点

読み込み操作(stopRequested)と書き込み操作(requestStop)の両方が同期されている必要がある

Page 10: Effective Java 輪読会 項目66-68

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; // 反映される}

Page 11: Effective Java 輪読会 項目66-68

volatile の失敗例

// 不完全 -同期が必要!private static volatile int nextSerialNumber = 0;

public static int generateSerialNumber() {

return nextSerialNumber++; // この操作はアトミックではない!// ので、タイミング次第では、複数のスレッドで// 同じシリアルナンバーを得てしまう(安全性エ

ラー)}

Page 12: Effective Java 輪読会 項目66-68

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();

}

Page 13: Effective Java 輪読会 項目66-68

可変データを共有しない

最善!

その際は、文書化しよう(方針が維持されるように)

フレームワークやライブラリを深く理解しよう(内部でスレッドが用いられているかもしれない)

Page 14: Effective Java 輪読会 項目66-68

事実上不変

「少しの間スレッドがデータオブジェクトを変更してから、オブジェクト参照を共有する処理だけを同期する」

「オブジェクトが再び変更されない限り、他のスレッドはさらに同期することなくオブジェクトを読み出すことができる」

// (”Java Concurrency in Practice” より引用)public Map<String, Date> lastLogin =

Collections.synchronizedMap(new HashMap<String, Date>());

Page 15: Effective Java 輪読会 項目66-68

安全な公開

フィールドを static にして、クラス初期化によろしくお願いする

フィールドを volatile にする

フィールドを final にする

フィールドをロックで保護する

コンカレントコレクションを用いる

Page 16: Effective Java 輪読会 項目66-68

まとめ

複数のスレッドが可変データを共有する場合、そのデータを読み書きするスレッドは同期を行う同期なしでは、あるスレッドの変更が他のスレッドから見えることは保証されない

同期しないことへのペナルティ:活性エラーと安全性エラーデバッグ困難!

タイミング依存、JVM 依存

スレッド間通信だけが必要な場合は、volatile もあるただし、正しく使用するのは難しい

詳しくは『Java Concurrency in Practice』を読もう(邦訳もあるよ)

Page 17: Effective Java 輪読会 項目66-68

項目67 過剰な同期は避ける

Page 18: Effective Java 輪読会 項目66-68

同期されたメソッドやブロック内で、制御をクライアントに譲らない オーバーライドされるように設計されているメソッドや関数オブジェクトを、同期された領域内で呼び出さない

<異質>なメソッドだから

メソッドが何をするかわからないため、例外、デッドロック、データ破壊の可能性がある

Page 19: Effective Java 輪読会 項目66-68

異質なメソッド呼び出し(例外編)

ObservableSet(pp.256-257)と、SetObserver(pp.257)

SetObserver#addedでObservableSet#removeObserverを呼び出すと、ConcurrentModificationExceptionがスローされる!(pp.257-258)

リストをイテレート中に、そのリストから要素を削除しようとしているから

Page 20: Effective Java 輪読会 項目66-68

異質なメソッド呼び出し(デッドロック編)

再び ObservableSet(pp.256-257)と、SetObserver(pp.257)

SetObserver#addedで、ExecutorService経由でObservableSet#removeObserverを呼び出すと、デッドロックが発生する!(pp.258)

ObservableSet.observersのロックは、既にメインスレッドによって獲得されているから

Page 21: Effective Java 輪読会 項目66-68

異質なメソッド呼び出し(地獄編)

もし、1. 同期された領域により保護されている不変式が一時的に不正になっている間に、

2. 同期された領域内から異質なメソッドを呼び出したら、

3. その異質なメソッドは首尾よくロックを獲得し(再帰的ロック)、

4. オブジェクトの内部状態をこっそり不正にしてしまう!(かもしれない)

ロックが用をなしていない再帰的ロックは、活性エラーを安全性エラーに変える可能性がある

Page 22: Effective Java 輪読会 項目66-68

異質なメソッド呼び出しの回避(オープンコール)

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);

}

Page 23: Effective Java 輪読会 項目66-68

異質なメソッド呼び出しの回避(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);

}

Page 24: Effective Java 輪読会 項目66-68

同期のコスト

ロックに費やされる CPU 時間

よりも、並行処理を行う機会損失

および、すべてのコアがメモリの一貫したビューを持つことを保証する必要性から生じる遅延

Page 25: Effective Java 輪読会 項目66-68

内部同期と外部同期

クラスが並行して使用されるのであれば、可変クラスをスレッドセーフにすべき

static フィールドへのアクセスは、必ず内部同期が必要

関係のないクライアント同士が、同じルールで同期できるとは限らないから

StringBufferと StringBuilder

StringBufferは、ほとんどの場合に単一スレッドから使用されるのに、内部的に同期を行っている

リリース 1.5 で、同期されていない StringBuilderによって置き換えられた

教訓:必要性が疑わしい場合には、内部的な同期を行わず、スレッドセーフでないことを文書化する

Page 26: Effective Java 輪読会 項目66-68

まとめ

デッドロックやデータ破壊を回避するため、同期された領域から異質なメソッドを呼び出さない(決して!)

より一般的には:同期された領域内で行う処理の量を制限する

可変クラスを設計する場合は、内部同期を検討する

並行性のため、過剰には同期せず、内部同期の採用が妥当でない場合はその旨をドキュメント化する

詳しくは『Java Concurrency in Practice』を読もう(邦訳もあるよ)

Page 27: Effective Java 輪読会 項目66-68

項目68

スレッドよりエグゼキューターとタスクを選ぶ

Page 28: Effective Java 輪読会 項目66-68

エグゼキューターフレームワークに親しもう

楽ちん便利

// ワークキューを作成ExecutorService executor =

Executors.newSingleThreadExecutor();

// runnable を実行のために発行executor.execute(runnable);

// 終了を指示executor.shutdown();

Page 29: Effective Java 輪読会 項目66-68

エグゼキューターサービスの多彩な機能

特定のタスクが完了するのを待つ

タスクの集まりの中のどれかのタスクや、すべてのタスクが完了するのを待つ

エグゼキューターサービスがきちんと完了するのを待つ

タスクが完了するごとに、1 つずつタスクの結果を取り出す

スレッドプールを作成する

Page 30: Effective Java 輪読会 項目66-68

エグゼキューターサービスの選択

小さなプログラムや、軽い負荷のサーバーなら:

Executors.newCachedThreadPool

設定不要、一般に「正しいことを行う」

高負荷の製品サーバーなら:

Executors.newFixedThreadPool

固定数のスレッドを持つプールを提供する

ThreadPoolExecutorを直接使用

最大限の制御が可能

Page 31: Effective Java 輪読会 項目66-68

Thread を直接使うのはもうやめよう

Thread

処理の単位と、処理を実行するための機構の両方

Runnable / Callable / エグゼキューターサービス

<タスク>(処理の単位)とその実行機構を、それぞれ適切に抽象化する

Page 32: Effective Java 輪読会 項目66-68

Timer を直接使うのももうやめよう

Timer

タスク実行のために単一スレッドしか使用していない

タイミングの精度に不安あり

例外がスローされると、動作しなくなる

ScheduledThreadPoolExecutor

複数スレッドをサポート

チェックされない例外をスローする例外からも回復

Page 33: Effective Java 輪読会 項目66-68

まとめ

詳しくは『Java Concurrency in Practice』を読もう(邦訳もあるよ)