こっから先はノートにメモってないんだよなぁ。
ちょっと読み直しつつかいてく。
--------------
48.可変データへの共有アクセス
Javaはlongやdouble型以外ならば読み書きがatomic(不可分)であることが保証されている。
(Tigerでjava.util.concurrent.atomicパッケージが追加された。参考:「Tiger (Java2 SE 1.5) で追加された並列プログラミング機能」)
だからといって、データの読み書きがatomicなら同期いらないじゃんということにはならない。
→Javaのメモリモデルではあるスレッドが変更した値を他のスレッドが参照できるとは限らない。
マルチスレッド対応でないシリアルナンバー生成器の例。
private static int serialNumber = 0; public static int getSerialNumber(){ return serialNumber++; }
マルチスレッド対応にさせるのならデータを書き換える部分をsynchronizedにする。
private static int serialNumber = 0; public static synchronized int getSerialNumber(){ return serialNumber++; }
他にも可変データにvolatile修飾子を付けるという手もある。
本では例として外部にフラグを公開して他のスレッドからオブジェクトの振る舞いを変更するクラスを挙げている。
まず、synchronized版。
private boolean stop = false; public void run(){ boolean done = false; while(!stopRequested() && !done ){ //処理 } } public synchronized void requestStop(){ stop = true; } private synchronized void stopRequested(){ return stop; }上記と同じ振る舞いをするvolatile版。
private volatile boolean stop = false; public void run(){ boolean done = false; while(!stopRequested() && !done ){ //処理 } } public void requestStop(){ stop = true; } private void stopRequested(){ return stop; }
そうするとさっきのserialNumberもvolatile宣言すれば終わりなのかという疑問も出てくるが、結果を先に言うとこれはアウト。
serialNumber++;は見た目は1ステップだが、本当は内部でデータの読み込み・加算演算・書き込みの3stepからなっているのでこれらをsynchronizedブロックでまとめてatomicにしないとマズイ。
基本的にvolatileが使えるのは上記のようなbooleanフラグぐらいなものだろう。
もっと応用的な使い方はIBMの記事、「Javaの理論と実践: volatile を扱う」参照。
IBMの記事は良記事が多い。synchronized とvolatile の違いをうまくまとめている。
volatile 変数は、可視性という特徴は synchronized と同じですが、synchronized のようなアトミック性を持っていません。これはつまり、スレッドは自動的に volatile 変数の最新の値を見るということです。
また本章の最後の方ではLazy Initialization(怠けた初期化; 必要になるまでオブジェクトを生成しないこと)のマルチスレッド対応方法についての手法もまとめている。
1.static初期化をしてLazy Initializationを諦める方法
2.Lazy Initialization処理が書かれているgetXXX()をまるごとsynchronized 化。
→同期化のコストはかかるが、一番確実。早期化のコストを減らそうとして二重チェックイデオムに走らない。
3.オンデマンド初期化ホルダークラスイデオム
→同期化コストがかからない。けどインスタンスフィールドには適用できない。
あと3.は個人的に読みづらい印象を受けるなぁ。
だいたい、オンデマンド初期化ホルダークラスイデオムの検索結果 1 件中 1 - 1 件目 (0.29 秒) だよ。あまり使われてないっぽいな。
--------------
49.同期とパフォーマンス
並列性を向上させるために同期されたブロック内の処理量は少なくするようにさせる。同期が必要ない処理はsynchronized の外に出す。
特に同期ブロック内でpublicやprotected等、オーバライド可能なメソッドの呼び出しは行わない。(=制御をクライアントに委ねてはならない)オーバライドの仕方によってはデッドロックの原因にもなる。(p186-187)
クラスが本当に複数のスレッドから使われるものなのか意識して設計する。
例えばStringBufferは大抵の場合、複数のスレッドで共有なんかしないので同期のコストは本来ならば不要。
(1.5 Tiger 以降ならStringBuilder使った方がいいね。)
同期を必要としないと判断したなら、そのことをドキュメント化することを忘れずに。
同期が必要な場合、同期が不要な場合、どちらでも多く必要とされるクラスの場合は?
→まず、同期無し版を作成。その後、作成したクラスの全てのメソッドを適切な同期ブロック内で呼び出すラッパークラスを同期アリ版としてリリースするのが適切。
--------------
50.waitループ
wait()は以下のように呼び出す。
synchronized (obj){ while(条件が成立していない) obj.wait(); //条件成立時に行う適切な処理 }
ループにすることで間違った通知(notify)から保護することが出来る。
全てwaitループイデオムを使ってwaitしているなら、notifyよりもnotifyAllを使って通知した方が正しい(安全)。通知内容に関係のない待ちスレッドは再び待機状態に入り、起こしたいスレッドは必ず起きるからだ。ただし、待ちスレッドが多い場合、パフォーマンスに悪影響を及ぼす。
--------------
51.スレッドスケジューラに依存するプログラムは書かない
・なかなか実行されないスレッドがあっても、Thread.yield(); とかして譲ったりはしないこと。JVMの実装により、その後の動きが異なるので移植性の低いプログラムとなる。
・同様にスレッドの優先順位も変更しないこと。
Thread.yield(); の唯一の使い道→テスト時に意図的に実行可能状態にあるスレッドを走らせてバグが出ないか調べるぐらい。
--------------
52.スレッドの安全性をドキュメント化しろ
まぁ、当たり前と言えば当たり前の話。
本ではスレッド安全性のレベルを5段階(immutable, thread-safe, conditionally thread-safe, thread-compatible, thread-hostile)に分け、これらを記述することを推奨している。
詳しくは「スレッド・セーフの特性について」を参照。
conditionally thread-safe(条件付きスレッドセーフ)の場合は特にドキュメントの書き方に気を付ける。
例えばクライアントがロックを取得しなければならないのならば、どのように取得すればよいのか、サンプルコードを記載する。
スレッドセーフクラスでクライアントにロックオブジェクトを提供する場合、柔軟性と脆弱性が紙一重な点に注意する。
//サービス拒否攻撃(lockObjを提供したクラスはもう動かない) synchronized (lockObj){ Thread.sleep(Integer.MAX_VALUE); }クライアントにロックを委ねている条件付きスレッドセーフクラスは常にこの攻撃にさらされる。
スレッドセーフクラスでの回避方法→プライベートなロックオブジェクトを使用。
private Object lockObj = new Object(); public void f(){ synchronized (lockObj){ //処理 } }
(そーいえばこのようなコードをどっかのクラスで見たなぁ...忘れてしまったが。)
--------------
53.スレッドグループは使わない
クラスjava.lang.ThreadGroupのこと。
唯一uncaughtExceptionぐらいは使えるらしいが、Tiger以後はThread.UncaughtExceptionHandlerインターフェースを実装する手法があるらしいので(参考:Tigerを使いこなす: スレッドでのデフォルト例外処理)マジで使うことはないクラスかも。