O'REILLY JavaScript 第5版を飛ばし読む(20章 HTTPの制御)
よく使う Ajax はどのような仕組みで動いているのか、概要をまとめる。
XMLHttpRequest
クライアントとサーバーの間でデータを伝送するための機能を、クライアント側で提供するAPIのこと。Ajaxプログラミングで利用される。
以下の構文で XMLHttpRequest
を生成する。
var myRequest = new XMLHttpRequest();
リクエストの送信
myRequest.open("GET", "/bar/foo.txt", true); myRequest.setRequestHeader("User-Agent, "XMLHttpRequest"); ...
open
メソッドでリクエストを初期化する。第一引数には使用するHTTPメソッドを指定する、"GET"、"POST"、"PUT"、"DELETE"など。第二引数にはリクエストを送信するURLを指定する。第3引数には非同期で操作を実行するかを指定する。デフォルトは true
で非同期実行。
必要であれば setRequestHeader
でヘッダを設定する。この時、ブラウザが自動で関連したクッキーをリクエストに追加する。
最後に send
関数でリクエストを送信する。リクエストのボディを send
関数の引数として渡す。GET
の場合は null
。
open
メソッドの第三引数に false
を指定した場合、同期リクエストとなるのでレスポンスが到着するまで、このメソッドは返らない。
myRequest.send(null);
非同期レスポンスの処理
open
メソッドの第三引数に true
を指定した場合、send()
メソッドはサーバーにリクエストを送信したらすぐにリターンする。
XMLHttpRequest の場合、イベントハンドラが onreadystatechange
プロパティに登録されるので、readystate
の値が変わるたびにイベントハンドラ関数が呼び出される。
readystate
はHTTPリクエストの状態を表す整数値で、以下の5つの値のうちいずれかを取る。
- 0:
open()
はまだ呼び出されていない - 1:
open()
は呼び出されたが、send()
は呼び出されていない - 2:
send()
は呼び出されたが、サーバはまだレスポンスを返していない - 3: サーバからデータを受信中である
- 4: サーバのレスポンスの受信が完了した
サーバから返されたHTTPのステータスコードを調べたい場合は、send()
呼び出しのあとで XMLHttpRequest オブジェクトの status
プロパティを調べる。
その他のレスポンス等のプロパティについては、以下の仕様を参照。
https://developer.mozilla.org/ja/docs/Web/API/XMLHttpRequest
Ajax の注意事項
Ajax は上記の XMLHttpRequest オブジェクトを用いて、Http をスクリプトにより制御し、ページをリロードすることなく Web サーバーと通信する技術である。 ブラウザは表示のためのデータ解析が不要であり、またやりとりするデータ量も少ないため、操作性やレスポンス時間を改善することができる。
ただし、実装時には以下のような注意点がある。
- レスポンスを待っている間のフィードバックを提供する必要がある。ネットワークの状態やサーバでの処理量によっては、想定外の時間がかかることもありうるが、 ユーザーには通信中であることがわからないため、アニメーション等で状況を適切に通知する必要がある。
- Ajax による通信では URL が更新されないため、ある特定の状態をブックマークすることができない。これを解決するためには、URL パラメータ等を使って、 URL 中にアプリケーションの状態をカプセル化する必要がある。
- ブックマークと同様の理由で、ブラウザの戻るボタン等も利用することができない。これを解決するためには、History API 等を使って、 適切なタイミングで URL を更新する必要がある。
感想
Ajaxの動作の仕組みが読む前より具体的にイメージできるようになった気がする。。。
ところでO'REILLY JavaScript 第5版の飛ばし読みはこれで最後の予定。なぜならこの本は会社の先輩から借りていたのですが、返却してしまったから・・・。 というか、第6版がすでに出ているんですね。第5版では最新のjavascriptの仕様が抜けていたので、6版もぜひ読みたいが。
O'REILLY JavaScript 第5版を飛ばし読む(13章 Webブラウザに組み込まれたJavaScript)
ここではDOMの構造やJavaアプレットの話が出てくるが、今回は割愛。
セキュリティ関係の話だけ少し取り出す。
JavaScript のセキュリティ
JavaScript ではできないこと
ユーザーの Web ブラウザ上で悪意のあるコードが実行されないよう、スクリプトの動作には以下のような制限がかけられている。
- クライアントのPC上のファイルやディレクトリに対して、読み出し、書き込み、作成、削除、表示ができない
- ソケットを開く、他のホストからの接続を受け付ける、といったネットワーク機能はサポートしていない
- クリック等のイベントなしに、ブラウザをオープンすることはできない
- そのプログラムがオープンしたウィンドウ以外は、ユーザーの確認なしにクローズできない
- リンクをマウスオーバーした際に、飛び先がステータス行に表示されるが、その内容をスクリプトで変更することはできない
- 1辺が100ピクセル以下のウィンドウ、画面サイズを超える大きさのウィンドウ、タイトルバーやステータス行のないウィンドウは作成できない
- 画面外にウィンドウを移動することはできない
- HTML の FileUpload 要素の Value プロバティには値を設定できない
- 同一出身ポリシー(後述)
同一出身ポリシー
同一出身ポリシーとは、以下のような内容である。
- あるスクリプトは、そのスクリプトを含むドキュメントと同じ出身のウィンドウやドキュメントのプロパティしか読み出せない
- 同様に、XMLHttpRequest オブジェクトで HTTP を制御する際も、リクエストを送信できるのはそのスクリプトを含むドキュメントがロードされた Web サーバーに対してのみ
- ドキュメントの「出身」は、そのドキュメントをロードした URL のプロトコルとホストとポート番号の組み合わせで決まる
これは、スクリプトを使って機密情報を盗めないようにする機構である。
例) 企業のイントラネット中のブラウザにロードされた悪意のあるスクリプトが空のウィンドウを開いたとする。 ユーザーがそのウィンドウからイントラネットのファイルを閲覧したとしても、同一出身ポリシーにより悪意のあるスクリプトはその内容を読み出すことはできない。
ただし、複数のサーバーを使用するような大規模な Web サービスでは、異なるサーバーからロードされたドキュメント間でプロパティを読み出さなければならない場合がある。
このような場合は各 Document
オブジェクトの domain
プロパティに同じ値を設定してやればよい。
document.domain
プロパティが同一である場合、2つのドキュメントは同一の出身とみなされ、相互にドキュメントのプロパティを読み出せるようになる。
デフォルトではドキュメントをロードしたサーバーのホスト名が設定されているが、この値は書き換えることができる。
例) orders.example.com と catalog.example.com からロードしたドキュメントの domain
プロパティを「example.com」に変更することで、同一出身ポリシーを回避できる。
クロスサイトスクリプティング
ある Web ページが、ユーザの入力したデータから HTML タグを削除せずに、ユーザの入力をそのまま利用してドキュメントのコンテンツを動的に生成していれば、クロスサイトスクリプトの脆弱性が存在する。 (HTML タグを削除する処理のことを「サニタイズ」と呼ぶ)
クロスサイトスクリプティングの流れは以下(一例)。
- クロスサイトスクリプトの脆弱性が含まれるサイトAがある。
- 悪意のあるユーザーがサイトAへのリンクを含むサイトBを用意する。サイトAへのリンクのクエリパラメータには、悪意のあるスクリプトをロードする script タグを含む値がセットされている。
- ユーザーが上記リンクからサイトAへアクセスすると、サイトAでクエリパラメータを用いてコンテンツが動的に生成され、悪意のあるスクリプトがロードされる。
- 悪意のあるスクリプトはクッキーの値等を読み出し、ユーザー情報等を取得することができてしまう。
感想
セキュリティの話はやっぱり変わらず大切。クロスサイトスクリプティングっていつも名前だけ覚えていて、内容なんだっけ。。。ってなる。
O'REILLY JavaScript 第5版を飛ばし読む(9章 クラスとコンストラクタとタイプ)
仕事で JavaScript 結構書くので、基礎的なところは抑えときたいと思って、O'REILLY JavaScript 第5版を飛ばし読みしています。まずは JavaScript でのクラスの実現方法から。
JavaScript におけるコンストラクタ、メソッド
JavaScriptのオブジェクトは、new
演算子またはオブジェクトリテラルで {}
を使用することで生成できる。
new
演算子と一緒に使う関数をコンストラクタと呼ぶ。
また、オブジェクトのプロパティとして呼び出される関数をメソッドと呼ぶ。
この形式で関数を呼び出すと、関数を呼び出したときに使われたオブジェクトが this
キーワードの値になる。
// コンストラクタ funtion Rectangle(w, h) { this.width = w; this.height = h; // メソッド this.area = fucntion() { return this.width * this.height; } } // オブジェクトの生成 var r = new Rectangle(2, 4); //または rect = {width: 2, height: 4} // 面積の計算 var a = rect.area;
プロトタイプと継承
上記の書き方には問題がある。それは生成されたすべての Rectangle
オブジェクトが、3つのプロパティを持つ点である。
width
と height
はオブジェクトごとに値が違うが、area
は共通の関数を参照するため、オブジェクトごとに持つ必要はない。
この問題を解決するのがプロトタイプである。
実はすべての javascript オブジェクトには、プロトタイプオブジェクトというオブジェクトへの参照が含まれる。(prototype
プロパティ)
new
演算子を使用してオブジェクトを生成すると、コンストラクタ関数の prototype
プロパティの値が、生成されたオブジェクトのプロトタイプになる。
生成されたオブジェクトは自身のプロトタイプのプロパティを参照することができる。
例えば、上記の例であればコンストラクタ Rectangle
の prototype
にメソッド area
を設定すれば、
すべての Rectangle
オブジェクトから共通の area
を参照できるようになる。これは Java でいう継承にあたる。
// コンストラクタ funtion Rectangle(w, h) { this.width = w; this.height = h; } // プロトタイプ Rectangle.prototype.area = fucntion() { return this.width * this.height; }
プロトタイプを適切に使用することで、使用するメモリを大幅に節約することができる。 また、あるオブジェクトを生成した後に、そのオブジェクトのプロトタイプにプロパティを追加しても、その値を継承することができる。
継承プロパティへのアクセス
オブジェクト o
のプロパティ p
を読み出す時は、まずオブジェクト o
にプロパティ p
があるかをチェックし、
なければオブジェクト o
のプロトタイプにプロパティ p
があるかをチェックする。
一方で、オブジェクト o
にプロパティ p
を書き込む時は、オブジェクト o
がプロパティ p
を持たない場合、
自動でオブジェクト o
に新しいプロパティ p
が追加される。
これは、仮にプロトタイプのプロパティ p
を書き換えると、同じプロトタイプを継承しているすべてのオブジェクトに影響が出るためである。
JavaScriptにおけるクラス
JavaScript にはクラスという正式の概念はない。ただし、上述したようにプロトタイプを用いて継承を実現しており、 Java などクラスベースのオブジェクト指向言語の長所をかなり実現している。
java などの言語と大きく異なる点は、プロパティやその型が予め決められておらず、動的に追加可能である点である。
※ これらの点について、最近のフロントエンド開発では ES6 以降の新しい構文や flow などのツールを用いることで改善が図られている。詳細はまたどこかで。。。
JavaScript における継承の例
以下に前述した Rectangle
クラスを継承したサブクラスである PositionedRectangle
クラスの例を示す。
サブクラスを作成する場合は、そのプロトタイプオブジェクトにスーパークラスのインスタンスを指定すればよい。
// Rectangle クラスに位置情報を追加した PositionedRectangle クラスのコンストラクタ funtion PositionedRectangle(x, y, w, h) { // call メソッドでスーパークラスのコンストラクタを呼び出す Rectangle.call(this, w, h); this.x = x; this.y = y; } // スーパークラスのコンストラクタを指定することで、サブクラスからスーパークラスのプロパティが参照可能になる PositionedRectangle.prototype = new Rectangle(); // スーパークラスの area メソッドのみを継承し、 width や heightの値自体は継承したくない場合 // プロトタイプから削除する delete PositionedRectangle.prototype.width; delete PositionedRectangle.prototype.height; // コンストラクタプロパティは、自身を参照するよう書き換える PositionedRectangle.prototype.constructor = PositionedRectangle;
上記のようにサブクラスを生成せずに、 別のクラスのプロトタイプをあるクラスのプロトタイプへコピーして別のクラスのメソッドを継承する事もできる。(詳細な説明は割愛)
感想
プロトタイプを用いたクラスと継承の仕組みが理解できたのは良かった。 種々の JavaScript ライブラリ使ってても、デベロッパーツールとか使ってデバッグするときは生の JavaScript 読むことになるし、そういうときに上記を知っていれば捗る気がする。
本には JavaScript におけるオブジェクトクラスの判定法とかいろいろ書いてあるけど、最近は ES6 使って Babel とかで変換して・・・ってのが普通だし、そこらのテクニックはそんなに意識しなくてよいのかなと思うので割愛。
Q学習でOpen AI GymのPendulum V0を学習した
強化学習のQ学習を勉強したので、せっかくなので Open AI Gymの 「pendulum v0」の学習を実装した。
この記事では Q 学習の基礎は知っているという前提で、学習環境や実装で苦労した点を説明する。
gym のインストール
- gym をインストール
$ pip install gym
- pybox2d をインストール
$ brew install swig $ git clone https://github.com/pybox2d/pybox2d $ cd pybox2d $ python setup.py build $ python setup.py install
Pendulum v0
Open AI Gym にはいくつかの強化学習評価用の環境が用意されているが、 今回は以下の「Pendulum v0」を選んだ。
https://github.com/openai/gym/wiki/Pendulum-v0
タスクの目的
振り子へ適切にトルクを与えることで、垂直に振り上げた状態を維持すること。
環境
「Pendulum v0」では以下の3つの値を環境の状態として取得できる。
- cos(theta): 振り子が角度 θ のときの cos の値
- sin(theta): 振り子が角度 θ のときの sin の値
- theta_dot: 振り子が角度 θ のときの 角速度
行動
エージェントは環境を観測し、振り子へ与えるトルクを決定し、振り子を回転させる。
報酬
エージェントは、行動の結果、以下の式で計算される報酬を受取る。
-(theta^2 + 0.1*theta_dt^2 + 0.001*action^2)
終了条件
実装者が適当に決めて良いということなので、 今回は1エピソードの中で1000 回の行動をし、その中で観測した sin の値の平均が 0.91 を上回ったら終了、とした。
Q学習による制御
実装したコードは以下。大体500エピソード前後で終了する。
https://gist.github.com/uu64/71529c63b374a9103486395811fc77bf
施行錯誤した点がいくつかあるので紹介する。
報酬の与え方
デフォルトの報酬に加えて、以下の条件で追加報酬を与えた。これによりできるだけ早い収束を目指した。
- sin の値が 0.98 より大きい場合は、最大 100 点のボーナス
- sin の値が -0.98 より小さい場合は、最大 100 点の罰則
行動の決定方法
行動の決定には epsilon-greedy 法を使ったが、以下の式で epsilon を決定した。これは 0 エピソード時点では 40% の確率でランダムに行動を選択するが、徐々にその確率を減少させ、251エピソード以降は Q が最大となるような行動を選択するようにしている。
epsilon = -0.0016*episode + 0.4
epsilon が小さすぎると、Q 値の学習が足りないのか、sin の値がなかなか向上しなかった。
状態の離散化の解像度
環境の状態を示す値はすべて連続値なので、適当な解像度で離散化する必要がある。
theta_dot が -8.0 から 8.0 の間で、最初はあまり荒くてもよくないかと思い、0.4刻み(40分割)で学習をしていたが、細かすぎるのか収束に 2000 エピソード近くかかった。
最終的には解像度を 0.8 刻み(20分割)まで落としたところ、500エピソード前後で収束するようになった。
感想
振り子が立ち始めるまでは簡単だったが、そこから収束のスピードや精度を向上させるための細かいパラメータ調整に時間がかかった。状態の属性数が増えるとなかなか厳しいのでは、という印象。
次は流行りの DQN でもう少し難しいタスクに挑戦したい。
mac にpyenv で python3 の環境を構築した
機械学習関係の勉強をしてみたくなったので、mac に python3 の環境を構築した。
pyenv のインストール
python2 が必要になることがあるかもしれないので、pyenv をインストールしてバージョン管理する
- pyenv のインストール
$ git clone https://github.com/pyenv/pyenv.git ~/.pyenv
- pyenv のパス設定
$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc $ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc $ echo 'eval "$(pyenv init -)"' >> ~/.zshrc
- 利用可能なバージョン確認
$ pyenv install --list
python3 のインストール
インストールした pyenv を使って python3 をインストールする
- python3 のインストール
$ pyenv install 3.6.5
zipimport.ZipImportError: can't decompress data; zlib not available
のエラーが出たので、以下を参考にxcode-select --install
をして解決した後、リトライインストールしたバージョンを確認して、インストールしたバージョンに切り替える
$ pyenv versions $ pyenv global 3.6.5 $ pyenv rehash
- python3 のバージョン確認
$ python --version
pip3 のインストール
python のパッケージマネージャ pip をインストールする。 が、python3 のインストール時に一緒にインストールされるようなので、バージョンだけ確認しておく。
- pip のバージョン確認
$ pip3 --version
Effective Javaを勉強します【第10章】
項目66. 共有された可変データへのアクセスを同期する
synchronized
予約語
Java ではマルチスレッドを取り扱うことができる。しかし複数のスレッドが同じオブジェクトを
同時に操作すると、プログラムが意図しない動作をする可能性がある。この問題を解決するのが
synchronized
予約語である。
synchronized
予約語を利用することで、メソッドやブロックがある時点で1つのスレッドのみで
実行されていることを保証することができる。
変数の読み書きの同期
Java の言語仕様は long
と double
型以外の変数については、アトミックであることを保証している。
すなわち、複数のスレッドにより同期なしでその変数が並行して変更されたとしても、どれかのスレッドにより
その変数に保存された値を返すことが保証されている。
ここで注意しなければならないのは、あるスレッドが書き込んだ値が、他のスレッドからも見えることは 保証されていないことである。
例えば、以下のコードについて考えてみる。プログラムが約1秒動作し、その後メインスレッドが stopRequested
を
true
に設定することで backgroundThread
が終了するように思えるが、実際はスレッドが止まることはない。
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; } }
スレッドが止まらない理由は、JVMの最適化により run
メソッド内部の while
ループが
以下のように書き換えられるためである。
if (!stopRequested) { while (true) { i++; } }
上記を防ぐためには、書き込みメソッドと読み込みメソッドの両方を同期する必要がある。 上記のコードは以下のように修正する。
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; while (!stopRequested()) { i++; } } }); backgroundThread.start(); TimeUnit.SECONDS.sleep(1); requestStop(); } }
まとめると、複数スレッドで可変データを共有する場合は、long
・double
以外であっても、
synchronized
で読み書きするスレッドを同期しなければならない。
volitile 修飾子
「変数の読み書きの同期」に記載したコードでは synchronized
が使われているものの、
その目的は相互排他のためではなく、スレッドが最新の値を見ることを指示するために使われている。
(stopRequested
を2つのスレッドが更新するわけではない)
このような時は、synchronized
ではなく volatile
修飾子を使うことも可能である。修正方法は以下。
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; } }
項目67. 過剰な同期は避ける
同期されたメソッドやブロック内で決して制御をクライアントに譲ってはいけない。
言い換えると、オーバーライドされるように設計されているメソッドや、関数オブジェクトの形式でクライアントから受け取ったメソッドを同期された領域内で呼び出してはいけない。そのようなメソッドはどのように動作するか分からないので制御することができず、場合によっては例外やデッドロック、データ破壊が発生する可能性があるため。
オープンコール
同期された領域から異質なメソッド(オープンコール)を呼び出してはいけない。
以下の ObservableSet
は上記を守っていないために、多くの問題を抱えている。
public class ObservableSet<E> extends ForwardingSet<E> { interface SetObserver<E> { void added(ObservableSet set, E element); } public ObservableSet(Set<E> set) { super(set); } private final List<SetObserver<E>> observers = new ArrayList<>(); public void addObserver(SetObserver<E> observer) { synchronized (observers) { observers.add(observer); } } public boolean removeObserver(SetObserver<E> observer) { synchronized (observers) { return observers.remove(observer); } } private void notifyElementAdded(E element) { synchronized (observers) { for (SetObserver<E> observer: observers) { observer.added(this, element); } } } @Override public boolean add(E element) { boolean added = super.add(element); if (added) { notifyElementAdded(element); } return added; } }
上記のコードは要素がセットに追加された時にクライアントが通知を受け取ることを可能にしている。
オブザーバーは addObserver
メソッドを呼び出すことで通知を受けるようになり、
removeObserver
メソッドを呼び出すことで通知を受けることを解除する。
この時、下記のように値を順にセットに追加し。23になったら自分を取り除くことを意図するとどうなるだろうか。
public static void main(String[] args) { ObservableSet<Integer> set = new ObservableSet<Integer>(new HashSet<Integer>()); set.addObserver(new SetObserver<Integer>() { public void added(ObservableSet<Integer> s, Integer e) { System.out.println(e); if (e == 23) { s.removeObserver(); } } }); for (int i = 0;i < 100;i++) { set.add(i); } }
実際には数字0から23までを表示して、その後に ConcurrentModificationException
がスローされる。
これは notifyElementAdded
メソッドが synchronized
ブロック内で SetObserver.added
(オープンコール)を
呼び出すことで生じている。
SetObserver.added
メソッドは removeObserver
メソッドを呼び出し、
さらにその中で observers.remove
メソッドを呼び出しているため、
observers
のイテレート中にその要素を削除しようとすることになり例外が発生してしまう。
この場合は例外が発生しただけだが、デッドロックが発生する場合もある。このように、同期された メソッドやブロックの中でオープンコールを呼んでは行けない。
一般的に、同期された領域内ではできる限り少ない処理を行うべきであり、必要最小限の処理の間でのみロックを獲得し、 その後は速やかに解放すべきである。
スレッドセーフな設計
クラスが並行して使用されるのであれば、可変クラスをスレッドセーフにすべきである。 また内部的に同期を行うことで、高い並行生を達成することが可能となる。 内部的に同期するための技法としては、次のようなものがある。
- ロック分割
- ロックストライピング
- 非ブロッキング並行性制御
クラスを内部的に同期させる場合は、そのことをドキュメント化すること。
項目68. スレッドよりエグゼキューターとタスクを選ぶ
java.util.concurrent
パッケージにはエグゼキューターフレームワークという
マルチスレッド処理を行うための仕組みが整っている。
まずは別スレッドにしたい処理を、Runnable
インタフェースを実装したクラスの run
メソッドに実装する。
public class TestRunnable implements Runnable { public void run() { // 実行したい処理 ... } }
あとは以下のようにスレッドを生成、実行すればよい。
ExecutorSerivice executor = Executors.newSingleThreadExecutor();
executor.execute(new TestRunnable());
処理が終わったら以下のコードでエグゼキューターを終了する。
executorService.shutdown();
スレッドプール
キューを実行するスレッドが2つ以上必要な場合は、スレッドプールを利用することが可能。
小さなプログラムや軽い負荷のサーバー処理には Executors.newCachedThreadPool
を利用すると良い。
ただし Executors.newCachedThreadPool
を利用すると、タスクはキューに入れられることなく直ちにスレッドに渡されるため、
利用可能なスレッドがなければ次々と新たなスレッドが生成されてしまう。
したがって高負荷のサーバーでは、固定数のスレッドを持つ Executors.newFixedThreadPool
を利用したほうがよい。
タスク
エグゼキューターはスレッドを実行する機構であり、実行する機能はタスクと呼ばれる Runnable
インタフェースや Callable
インタフェースに記述する。従来の Thread
は処理の単位と処理を実行する機構が混在していたが、エグゼキューターとタスクによりそれらが分離され、より柔軟にスレッドを扱うことができるようになった。
項目69. wait と notify よりコンカレンシーユーティリティを選ぶ
Java 5以降では、wait と notify を使用するよりも、用意されているコンカレンシーユーティリティ(java.util.concurrent)を利用したほうが良い。
java.util.concurrent 内の高レベルのユーティリティは、エグゼキューターフレームワーク、コンカレントコレクション、シンクロナイザーの3つに分類され、本項目では後者2項目を説明する。
コンカレントコレクション
List、Queue、Map などの標準コレクションインタフェースの高パフォーマンスな並行実装を提供している。これらには独自の同期管理の仕組みが実装されているため、並行な活動を排除することは不可能である。すなわち、コンカレントコレクションをロックしても効果はなく、プログラムを遅くするだけなので注意すること。
コンカレントコレクションをアトミックに操作するために、いくつかのメソッドが用意されている。例えば、ConcurrentMap
の putIfAbsent
メソッドなど。
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; }
コレクションインタフェースによっては、ブロックする(操作が完了するまで待つ)操作が用意されている。
例えば BlockingQueue
は、キューが空なら待つ take
メソッドが用意されている。
シンクロナイザー
シンクロナイザーは、スレッドが他のスレッドを待つことを可能にするオブジェクトである。
CountDownLatch
、Semaphore
、CyclicBarrier
、Exchanger
の4種類がある。
以下は CountDownLatch
を用いたアクションの並列実行処理時間を計測するプログラムの例である。
wait
と notify
を利用して同様の実装をするより簡単である。
// executor: アクションを実行するエグゼキューター // concurrency: 同時実行スレッド数 // action: 計測対象のアクション 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() { @Override public void run() { // スレッドが実行可能になったら通知する ready.countDown(); try { // start.countDown()が1回実行されるまで待つ start.await(); action.run(); } catch(InterruptedException e) { Thread.currentThread().interrupt(); } finally { // スレッドの処理が終了したら通知する done.countDown(); } } }); } // すべてのスレッドが実行可能になるまで待つ // すなわち concurrency 回 ready.countDown() が呼ばれるまで先には進まない ready.await(); long start = System.nanoTime(); start.countDown(); // すべてのスレッドの処理が終了するまで待つ // すなわち concurrency 回 done.countDown() が呼ばれるまで先には進まない done.await(); return System.nanoTime() - start; }
wait
と notify
の使い方については、今後は直接使用する機会は少ないと思うので割愛。
項目70. スレッド安全性を文書化する
クラスのインスタンスや static
のメソッドが並行して使用された場合に、そのクラスはどのように振る舞うか文書化しなければならない。
文書化するときの注意点としては、以下のようなものが挙げられる。
synchronized
修飾子は実装の詳細であるため、Javadoc に含むべきではない。- スレッド安全性に関しては、以下のサポートするスレッド安全性のレベルを明確に文書化しなければならない。
- 不変(immutable): このクラスのインスタンスは定数のように振る舞い、外部の同期は必要ない
- 無条件スレッドセーフ(unconditionally thread-safe): このクラスのインスタンスは可変だが、外部同期は不要であり、並列実行可能な内部同期の仕組みが実装されている
- 条件付きスレッドセーフ(conditionally thread-safe): 安全に並列実行するために、いくつかのメソッドは外部動機が必要
- スレッドセーフではない(not thread-safe): このクラスのインスタンスは可変であり、ここのメソッド呼び出しはクライアントにより外部同期する必要がある
- スレッド敵対(thread-hostile): このクラスは、たとえすべてのメソッドが外部同期されたとしても、並列実行すべきではない
- 条件付きスレッドセーフのクラスを文書化する際は、外部同期が必要なメソッドと、そのために獲得すべきロックを明記する必要がある。
- 誰でもアクセス可能なロックをクラスが使用すると、ロックを長期間保持することによるサービス拒否攻撃を受ける可能性がある。これに対して、無条件スレッドセーフのクラスの場合、以下のようなプライベートロックオブジェクトを利用することができる。継承のために設計したクラスにおいては、サブクラスとスーパークラスの間の干渉を防ぐことができるため、特に有用。
private final Object lock = new Object(); public void foo() { synchronized(lock) { ... } }
項目71. 遅延初期化を注意して使用する
遅延初期化は、フィールドの値が必要となるまで、そのフィールドの初期化を遅らせること。 ほとんどの状況では遅延初期化よりは普通の初期化が望ましい。
特に複数スレッドで遅延初期化を使用する場合、フィールドの初期化時にロックが必要となるためアクセスコストが増加し、 また適切に同期が行われない場合深刻なバグとなる可能性があるため、注意しなければならない。
同期されたアクセッサー
複数スレッドから遅延初期化を使用する場合、一般的には以下のような同期されたアクセッサーを使用する。
private FieldType field; synchronized FieldType getField() { if (field == null) { field = computeFieldValue(); } return field; }
遅延初期化ホルダークラスイデオム
static
フィールドに対するパフォーマンスのために遅延初期化を使用する場合は、遅延初期化ホルダークラスイデオムを使用する。
これは「クラスが利用されるまでクラスが初期化されないことが保証される」という言語仕様を利用している。
private static class FieldHolder { static final FieldType field = computeFieldValue(); } static public FieldType getField() { return FieldHolder.field; }
上記の場合、初めて getField
メソッドが呼び出されるときに、初めて FieldHolder
クラスが初期化される。
二重チェックイデオム
インスタンスフィールドに対するパフォーマンスのために遅延初期化を使用する場合は、二重チェックイデオムを使用する。 このイデオムは、フィールドの初期化が行われた後にアクセスされた場合のロックのコストを回避する。
private volatile FieldType field; public FieldType getField() { FieldType result = field; if (result == null) { //1回目検査 synchronized(this) { result = field; if (result == null) { //2回目検査 field = result = computeFieldValue(); } } } return result; }
上記では1回目の検査で、初期化されているかをチェックしている。
すでに初期化されている場合そのまま result
を返すため、ロックによるコストを回避することができる。
初期化されていない場合、synchronized
でロックをして2回目の検査を実施した上で、初期化を実施する。
(このため二重チェックイデオムと呼ばれる)
変数 field
は synchronized
修飾子の内外からアクセスされるため、volatile
宣言する必要があることに注意すること。
項目72. スレッドスケジューラに依存しない
複数スレッドが実行可能な場合、スレッドスケジューラにより各スレッドの実行時間が決定されるが、この決定方法の詳細はオペレーティングシステムにより異なる可能性がある。 複数スレッドを扱うプログラムを書く際は、パフォーマンスや振る舞いが、あるオペレーティングシステムのスレッドスケジューラに依存しないよう注意する必要がある。
スレッドスケジューラに依存しないようにするためには、実行可能なスレッドの平均数がプロセッサの数を大きく超えないことを保証すればよい。 そのためには、有益な処理をしていないスレッドは動作しないようにする必要がある。
エグゼキュータフレームワークであれば、スレッドプールを適切な大きさにして、タスクを適度に小さくし、各スレッドが独立するよう設計すること。
項目73. スレッドグループを避ける
スレッドグループは、もともとセキュリティ向上のためにアプレットを隔離する仕組みとして考案されたが、今日では利用する機会はない。 もしスレッドの論理的なグループを扱うクラスを設計するのであれば、スレッドプールエグゼキューターの利用などを検討すること。