LoginSignup
17
23

More than 5 years have passed since last update.

Effective Java読書会10日目 - 並行性

Last updated at Posted at 2014-06-05

前回の記事はこちら↓
Effective Java読書会9日目 - 例外

[扱うテーマ一覧]
項目66 共有された可変データへのアクセスを同期する
項目67 過剰な同期は避ける
項目68 スレッドよりエグゼキューターとタスクを選ぶ
項目69 waitとnotifyよりコンカレンシーユーティリティを選ぶ
項目70 スレッド安全性を文書化する
項目71 遅延初期化を注意して使用する
項目72 スレッドスケジューラに依存しない
項目73 スレッドグループを避ける

前置き

全てはパフォーマンスのためにw

シングルスレッドに比べると、期待通りに動かない可能性が高い
テストも大変だし、エラーを再現するのも困難
とは言え、マルチコアが当たり前の世の中では、パフォーマンスを考えると避けて通れない

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

同期は読み書き両方必ずね!

同期とは?

スレッド内の操作がアトミックであること(要するに排他制御できていること) ← 書き込みの同期
どのスレッドから見ても同じ値になっていること ← 読み込みの同期

アトミックとは?

他から見て1つの操作に見える状態
操作が終わるまで他から見えないし、失敗したら操作前に戻っていないとダメ

long型とdouble型がアトミックでない理由

32bitのVMは、64bit変数を2つのスレッドに分けて処理するため
64bitのVMだったらアトミックになるのかは謎

Thread.stopは絶対使わないこと!

代わりに、初期値がfalseのbooleanを最初のスレッドにポーリングさせて、2番目のスレッドでtrueをセットして止める方法が良い

synchronized修飾子

1つのスレッドがある時点で1つのメソッドやブロックを実行していることを保証する
同期の説明にある通り、読み込みメソッドと書き込みメソッドの両方を修飾しないと無意味

適切に同期された協調的スレッド終了
public class StopThread {
    private static boolean stopRequested;
    //書き込みメソッドを同期
    private static synchronized void requestStop(){
        stopRequested = true;
    }
    //読み込みメソッドを同期
    private 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;
                //同期しないと、ここでstopRequestedの中身が保証されないので、活性エラーが起きる
                while(!stopRequested())
                    i++;
            }
        });
        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
}

変数をアトミックにしたいなら、volatile修飾子よりAtomicLongクラスを使う

volatileは最後に書き込まれた値が入っていることが保証される
例えば、public static volatile long hogeHoge;ってすると、64bitな値が入ってきても2スレッドで処理した結果が入った後の値が入ってる(アトミックになる)
ただ、わざわざこんな書き方するよりおとなしく java.util.conccurent.atomicAtomicLong を使った方が分かりやすい

そもそもマルチスレッドで可変データを共有しないのが一番

どうしても必要な場合は、読み書きするスレッドの同期は必須

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

状況により、パフォーマンス低下、デッドロック、予想がつかない振る舞いをもたらす危険性がある
活性エラーと安全性エラーを回避するためには、同期されたメソッドやブロック内で決して制御をクライアントに譲っていはいけません

活性エラー(liveness failure)とは?

ループ処理で変数を同期していないと、VMがコード最適化の結果、変数へのアクセスをループの外に巻き上げ(hoisting)してしまい無限ループが起きること

活性エラーの例
//実際に書いたコード
while (!stopRequested)
    i++;

//VMが最適化したコード
//変数が巻き上げられてしまう
if (!stopRequested)
    //無限ループ
    while(true)
        i++;

安全性エラー(safety failure)とは?

プログラムが誤った結果を計算してしまうこと

安全性エラーの例
private static volatile int nextSerialNumber = 0;

//同期していないと、1スレッド目のインクリメント結果が書き戻される前に、2スレッド目が値を読みだしてしまい、期待したインクリメントが行われない
public static int generateSerialNumber(){
    return nextSerialNumber++;
}

Observerパターンでの注意点

要素の変化を通知して処理を行うObserverパターンを同期する場合、同期された側からは何やってるか分からないcallbackみたいなメソッド(異質なメソッド)が呼ばれることで、以下の問題が起こる危険性がある
1. リストへのイテレート中に、リストから削除しようとして例外発生
2. メインスレッドがリストをロックしている時に、バックグラウンドスレッドがロックを獲得しようとしてデッドロック発生

オープンコール

Observerパターンでsynchronizedされたcallbackメソッドなどを呼ぶ場合、リストのスナップショットを配列(CopyOnWriteArrayList)に入れて、ロックの外から処理してやれば良い。この方法をオープンコールと呼ぶ。
ただし、このメソッドを処理している間、他のスレッドは不必要に待たされ、パフォーマンスが低下する恐れがあるので、ロックを獲得してから解放するまでの間は短い方が良い。
というか、そもそもこういう使い方しない方が良いと思う。。。

パフォーマンスについて

同期処理のコストは
1. 並行処理を行う機械損失
2. 全てのコアがメモリの一貫したビューを持つことを保証するための遅延
で、過剰に同期を行うと、JVMのリソースを奪ってしまう

同期は分かる範囲で必要最低限で

可変クラスを設計する場合、そのクラスが並行して独自の処理を行うべきか考える
必要ならスレッドセーフにして文書化しておく
必要ないなら同期しない

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

エグゼキューターフレームワークで新しいタスクを作成
ExecutorService executor = Excutors.newSingleThreadExecutor();
タスクを実行する
excutor.execute(runnable);
タスクを終了する
excutor.shutdown();
スレッドプールをキャッシュする場合(小さなプログラムや軽い負荷向き)
ExecutorService executor = Executors.newCachedThreadPool();
スレッドプールを固定する場合(高負荷向き)
ExecutorService executor = Executors.newFixedThreadPool();
//またはThreadPoolExecutorを直接使う

java.util.Timerの代わりに ScheduledThreadPoolExecutor を使うと良い

項目69 waitとnotifyよりコンカレンシーユーティリティを選ぶ

コンカレンシーユーティリティ(何故にカタカナ)
java.util.conccurent

詳細はjavadoc読んでw
http://docs.oracle.com/javase/jp/6/api/java/util/concurrent/package-summary.html

要するに、waitとnotify(とnotifyAll)を使って頑張ってスレッドセーフにするより、最初から並行処理用に内部的に同期されているjava.util.conccurentを使っとけってこと。

3つのカテゴリ

  1. エグゼキューターフレームワーク(項目68)
  2. コンカレント(並行処理)コレクション
  3. シンクロナイザー

コンカレントコレクション

List, Queue, Mapなどの並行実装用
独自の同期を内部的に管理しているので、ロックしても意味無い(というか遅くなるだけ)

ConccurentHashMapを使ってString.intern()が6倍以上早くなるらしい最適化
private static final ConcurrentMap<String, String> map =
    new ConcurrentHashMap<String, String>();

public static String intern(String s) {
    //検索操作に最適化されているので、毎回putIfAbsent呼ぶより、getして必要な分だけ呼んだ方が早い
    String result = map.get(s);
    if (result == null) {
        result = map.putIfAbsent(s, s);
        if (result == null)
            result = s;
    }
    return result;
}

Collections.synchronizedMapやHashtableをsynchronizedしていたら、ConcurrentHashMapに置き換えるだけで劇的にパフォーマンス良くなるらしい。

String.intern()とは?

対象の文字列で1つのStringオブジェクトを返してくれる
String s = "hoge";
String t = new String("hoge");
の場合、オブジェクトが違うからs == tにはならず、
s.equals(t)ってやってオブジェクトの中身を比較しないといけないんだけど、
intern()使うと、s.intern() == t.intern()とできるのでequals()より高速に処理できる
オブジェクトが同じだからequals()自体も高速になるかも?
それより何よりメモリの節約になる
と言っておきながら、intern()自体が遅いので多様は禁物。使うなら上記の最適化をすると良さげ。

BlockingQueue

生産者スレッドがワーク項目をワークキューに入れると、
消費者スレッドがそれを取り出して処理する
逆に言うと、ワークキューが空なら消費者スレッドは待ち続ける
ExecutorServiceは上記のブロックする操作を持つように拡張されている

シンクロナイザー

CountDownLatchを使った並行実行を計測する簡単なフレームワーク
public static long time(Executor executor, int concurrency,
        final Runnable action) throws InterruptedException {
    final CountDownLatch ready = new CountDownLatch(concurrency);
    final CountDownLatch start = new CountDownLatch(1);
    final CountDownLatch done = new CountDownLatch(concurrency);
    for (int i = 0; i < concurrency; i++) {
        executor.execute(new Runnable() {
            public void run() {
                ready.countDown();  //タイマーに準備OKを伝える
                try {
                    start.wait();  //同僚が準備OKになるまで待つ
                    action.run();
                } catch (InterruptedException e) {
                    Thread.currrentThread().interrupt();
                } finally {
                    done.countDown();  //タイマーに終了したことを伝える
                }
            }
        });
    }
    ready.await();  //全てのワーカーが準備OKになるのを待つ
    long startNanos = System.nanoTime();  //ナノ秒まで計測できる(Java5以降)
    start.countDown();  //そして、スタート!
    done.await();  //全てのワーカーがゴールするのを待つ
    return System.nanoTime() - startNanos;
}

項目70 スレッド安全性を文書化する

javadocにsynchronized修飾子は含まれない、っていうかこれ付いてたからってスレッドセーフかどうか分からないから、正しく実装されたら、コード読まなくて済むように、安全性のレベルを明確に文書化しておくことが重要。

安全性のレベル

不変(immutable)

このクラスのインスタンスは定数のように見え、外部での同期は必要ない。
例:String, Integer, BigInteger

無条件スレッドセーフ(unconditionally thread-safe)

このクラスのインスタンスは可変だけど、全てのメソッドが内部同期を含んでいる。
例:Random, ConccurentHashMap

条件付きスレッドセーフ(conditionally thread-safe)

メソッドの一部が外部同期を必要としている。
例:Collections.synchronized

スレッドセーフでない(not thread-safe)

このクラスのインスタンスは可変で、並行して使うためには外部同期が必要。
例:ArrayList, HashMap

スレッド敵対(thread-hostile)

このクラスは、全てのメソッドを外部同期しても、安全に並行処理できない。
例:System.runFinalizersOnExit

スレッド安全性アノテーション

Immutable
ThreadSafe
NotThreadSafe

プライベートロックオブジェクトイデオム - サービス拒否攻撃を阻止する
private final Object lock = new Object();

public void foo() {
    synchronized(lock) {
        ...
    }
}

項目71 遅延初期化を注意して使用する

必要でなければするな。

遅延初期化が有効なケース

クラス自体はよく呼ばれるが、そのフィールドはインスタンスの一部でしかアクセスされず、フィールド自体の初期化にもコストを伴う場合

スレッドセーフにする手法が書かれているけど、こういうシチュエーションがあんま無さそうだし、おとなしく普通に初期化した方が安全なので割愛w

項目72 スレッドスケジューラに依存しない

頑強で応答性がよく、移植可能なプログラムを書くための最善の方法は、
実行可能なスレッドの平均数が、プロセッサの数よりも非常に大きくならないことを保証すること。

実行可能なスレッドの数を少なく保つための主要な技法は、個々のスレッドに何らかの有益な処理を行わせて、
それからさらなる処理を持たせること。
スレッドが有益な処理を行っていない場合には、スレッドは動作すべきではない。

スレッドが上手く動作していないからと言って、安直にThread.yield()を呼ばない。

項目73 スレッドグループを避ける

ThreadGroupは使わない、以上w

17
23
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
23