ポイントというか心にとまったことのアウトプットです。
※個人的な重要度のバイアスがかかっているので、「べつにいいかー」と思ってたりすることはメモしてません。
※手を動かして理解したことなど、間違いを含む可能性があります。
※手元の環境は断りがなければ基本はjava 7 をIntelliJ IDEA Ultimate on OSX で動かしてます。
「スレッド」
- 実行状態=スレッド
- CPUコア数以上のスレッド タイムスライス
- スレッド同士の実行譲り合い
- この譲り合い(CPUの奪い合い)は予測不可能
- マルチスレッドプログラミング
- 共有データの扱いに気をつける
- 同期処理、排他制御
- 共有データの扱いに気をつける
- スレッド横取りが起きない操作=アトミックな操作
- ロック
スレッド間で共有されないのは、変数の「値」。参照型変数の場合は、共有されうるので注意
昨今開発者自身でスレッド生成することは少なくなっている(FWに隠蔽されるため)ので、複数スレッドから呼ばれても問題ないコードを書く技術が重要
Threadのjoinインスタンスメソッドは、start()のあとのrun()が終了するまでそこで待つ。
- p.443のリスト17.5のjoin()は意味が無いが、join()の後に通常は何か書くはず(待ち合わせ後の処理)
Runnableインターフェース
- @FunctionalInterface
- new Thread のコンストラクタ引数にラムダ式を記述可能
- でも処理が小さい(ラムダ式で記述するような)のに、Thread作るのはそもそもおすすめしない
Thread拡張を使うか、Runnableインターフェースを使うか
- Runnableインターフェースを利用すべき(この本的には)
- 拡張よりも委譲の方が依存性が弱まる
- エントリポイントの実装と、Threadの生成という役割分担ができる
- Runnableインターフェースを利用すべき(この本的には)
スレッドプール
- 一度作成したスレッドを使い回す(プールしておく)
- java.util.concurrent
- ExecuterServiceのexecute()に、Runnableインターフェースの実装クラスのオブジェクトを渡す。(new Thread()の引数にRunnableを渡すのと同じイメージ)
- ExecuterServiceのshutdown()とawaitTermination()
Runnableは値を返せないけど、Callableは値を返せる
- Future
- getメソッドでスレッドの戻り値を取得。まだ実行中だったら、実行が終わるまで停止する。タイムアウト値も設定可能。
- CompletableFuture(java8)
- 実行結果を待つコールバックが書ける
- ☆thenで並列処理結果を待ちながらコールバックをつなぐ処理が書ける
- Future
synchronized
- モニタロック
- モニタロックはオブジェクト、Classオブジェクトに対してsynchronized呼び出しで獲得する
- あるスレッドがモニタロックを獲得中、ほかのスレッドはそのモニタロックを獲得できない
- モニタロック
public class TakudoNotSyncronized {
private int _cnt = 0;
public int getCnt(){
return _cnt;
}
public void inc(){
_cnt ++;
}
}
public class TakudoSyncronized {
private int _cnt = 0;
public int getCnt(){
return _cnt;
}
public synchronized void inc(){
_cnt ++;
}
}
TakudoNotSyncronized obj1 = new TakudoNotSyncronized();
Thread th1 = new Thread(() -> {
for(int i = 0; i < 1000; i++){
obj1.inc();
}
});
Thread th2 = new Thread(() -> {
for(int i = 0; i < 1000; i++){
obj1.inc();
}
});
th1.start();
th2.start();
th1.join();
th2.join();
System.out.println(obj1.getCnt()); // 2000になるときもあるけどならないときもある
TakudoSyncronized obj2 = new TakudoSyncronized();
Thread th3 = new Thread(() -> {
for(int i = 0; i < 1000; i++){
obj2.inc();
}
});
Thread th4 = new Thread(() -> {
for(int i = 0; i < 1000; i++){
obj2.inc();
}
});
th3.start();
th4.start();
th3.join();
th4.join();
System.out.println(obj2.getCnt()); // 2000になる!
↑はsynchronized修飾子。synchronized文でも同様のことが可能。
- synchronized文の方がロックを短くできる&任意のオブジェクト参照を指定できるので、自由度高い
クラスのモニタロックは、共有範囲が広いため非推奨
モニタロックに不変オブジェクト(String, Integerなど)を指定するとバグの元になる
volatile 修飾子
- フィールドに使える
- 同期処理にフィールドの読み書きだけ程度であれば、synchronizedよりもvolatileの方が効率的
- long,doubleの代入もアトミック操作にできる(long,double以外の変数への代入はアトミック操作)
- 変数の読み・書きのスレッド割り込みを防ぐものであり、一連した操作(たとえば++演算子。読み→計算→代入をしている)にはスレッド割り込みの問題はあるので、そこは要注意
明示的ロック
- java.util.concurrent.locks
- unlockを忘れないよう、tryで囲い、finallyでunlockするようにする
- ReadWriteLockで、読み書きに対応するロックを使うことができる
アトミック処理
- java.util.concurrent.atomic
- CAS(Compare And Swap)
- AtomicInteger, AtomicLongなどを提供
- volatile 修飾子は不要
- 優位なのは実行速度(比較的)
通知
- wait()で待機させ(モニタロックの解放。CPU使用しない)とnotify()でwait()で止まっていたところから再開
- 待つ側はconsumer、起こす側をproducer と呼ぶ
デッドロックは、ロックを入れ子にし、順番が異なるものを呼び出すと起こる。
- ロックを入れ子にしない、入れ子にするときは順番を合わせれば、デッドロックを回避できる
- ☆VisualVMや、☆Mission Controlでデッドロックを検出可能
コレクション用の同期オブジェクト Collectionsクラスで提供。
- 読み込み・書き込みの間に割り込みの可能性があるので、一連で操作したいときはsynchronizedで囲う必要あり
並行コレクションは実行性能いいけど、やはり割り込みには注意すべし