スレッドとNIO
デバイスとアプリ(Java側)の考え方については、前段の記事で書きました。ではJava側どのようなアーキテクチャが実現されているのか?
次の記事を見つけました。
Notes on the Asynchronous I/O implementation
非同期I / Oの実装に関する注意
( Notes on the Asynchronous I/O implementation )
固定スレッドプール
サイズNの固定スレッドプールに関連付けられた非同期チャネルグループは、カーネルからのI/Oまたは完了イベントを待機するN個のタスクを送信します。各タスクは、単にイベントをデキューし、必要なI/O完了を実行してから、結果を消費するユーザの完了ハンドラに直接ディスパッチします。完了ハンドラが正常に終了すると、タスクは次のイベントの待機に戻ります。キャッチされなかったエラーまたはランタイム例外が原因で完了ハンドラーが終了した場合、タスクは終了し、すぐに新しいタスクに置き換えられます。これを次の図に示します。
この構成は比較的単純であり、適切に設計されたアプリケーションに優れたパフォーマンスを提供します。オンデマンドでのスレッドの作成や、アイドル時のスレッドプールのトリムバックはサポートされていないことに注意してください。また、無期限にブロックする完了ハンドラーの実装を持つアプリケーションには適していません。完了ハンドラーですべてのスレッドがブロックされている場合、I/Oイベントを処理できません(たとえば、オペレーティングシステムに受け入れられた接続をキューに入れるように強制します)。チューニングには、Nに適切な値を選択する必要があります。
ユーザ提供のスレッドプール
ユーザ提供のスレッドプールに関連付けられた非同期チャネルグループは、ユーザの完了ハンドラーを呼び出すだけのタスクをスレッドプールに送信します。カーネルからのI/Oおよび完了イベントは、ユーザアプリケーションからは見えない1つ以上の内部スレッドによって処理されます。この構成を次の図に示します。
この構成は、次の例外を除いて、ほとんどのスレッドプール(キャッシュまたは固定)で機能します。
- スレッドプールは、無制限のキューイングをサポートする必要があります。
- executeメソッドを呼び出すスレッドは、タスクを直接実行してはなりません。つまり、内部スレッドは完了ハンドラーを呼び出しません。
- 古いエディションのWindowsでは、スレッドのキープアライブを無効にする必要があります。この制限は、I / O操作がカーネルによって開始スレッドに関連付けられているために発生します。
この構成は、I/O操作ごとのハンドオフにもかかわらず優れたパフォーマンスを提供します 。オンデマンドでスレッドを作成するスレッドプールと組み合わせると、長期間(または無期限に)ブロックする必要がある完了ハンドラーを持つアプリケーションでの使用に適しています。内部スレッドの数であるMの値は 、APIで公開されておらず、構成するためにシステムプロパティが必要です(デフォルトは1)。
デフォルトのスレッドプール
独自の非同期チャネルグループを作成しない単純なアプリケーションは、自動的に作成されるスレッドプールが関連付けられているデフォルトのグループを使用します。このスレッドプールは、上記の構成のハイブリッドです。これは、オンデマンドでスレッドを作成するキャッシュされたスレッドプールです(ブロッキング操作を呼び出す完了ハンドラーを使用するさまざまなアプリケーションまたはライブラリによって共有される場合があるため)。
固定スレッドプール構成と同様に、 イベントをデキューし、ユーザーの完了ハンドラーに直接ディスパッチするN個のスレッドがあります。Nの値は、デフォルトでハードウェアスレッドの数になりますが、システムプロパティで構成できます。Nスレッドに加えて、イベントをデキューし、タスクをスレッドプールに送信して完了ハンドラーを呼び出す1つの追加の 内部スレッドがあります。この内部スレッドは、すべての固定スレッドがブロックされているか、そうでなければビジー状態で完了ハンドラーを実行しているときに、システムがストールしないことを保証します。
I/O操作がすぐに完了するとどうなるか?
I/O操作がすぐに完了すると、開始スレッド自体がプールされたスレッドの1つである場合、APIは開始スレッドによって完了ハンドラーを直接呼び出すことを許可します。これにより、スレッドのスタックに複数の完了ハンドラーが存在する可能性があります。次の図は、読み取りまたは書き込みメソッドがすぐに完了し、完了ハンドラーが直接呼び出されるスレッドスタックを示しています。次に、完了ハンドラーは、すぐに完了する別のI/O操作を開始するため、その完了ハンドラーが直接呼び出されます。
デフォルトでは、実装では、スレッドスタック上のすべての完了ハンドラーの終了を要求する前に、最大16のI/O操作を開始スレッドで直接完了することができます。このポリシーは、スレッドがすぐに完了する多くのI/O操作を開始した場合に発生する可能性がある、スタックオーバーフローと飢餓を回避するのに役立ちます。このポリシー、およびスレッドスタックで許可される完了ハンドラーフレームの最大数は、必要に応じてシステムプロパティによって構成されます。将来、APIに追加すると、アプリケーションは、すぐに完了するI/O操作をどのように処理するかを指定できるようになる可能性があります。
ダイレクトバッファ
非同期I/O実装は、直接バッファーで使用するために最適化されています。SocketChannelsと同様に、すべてのI/O操作は直接バッファーを使用して実行されます。アプリケーションが非直接バッファーを使用してI/O操作を開始すると、実装によってバッファーが直接バッファーに透過的に置き換えられます。
デフォルトでは、ダイレクトバッファに割り当てることができる最大メモリは、最大Javaヒープサイズ(Runtime.maxMemory)と同じです。これは、必要に応じて、MaxDirectMemorySize VMオプション(例:-XX:MaxDirectMemorySize = 128m)を使用して構成できます。
jconsoleのMBeanブラウザを使用して、直接バッファに関連付けられたリソースを監視できます。
後の記事でも書くつもりですが、「ByteBuffer」にダイレクトバッファを利用するかどうかの分岐があります。
// 1バイトずつ読込
ByteBuffer bufffer = ByteBuffer.allocate(1);
このようにコーディングしているとバイトバッファはヒープメモリに展開されるようです。つまりガベージコレクションの対象となるわけですね。
// 1バイトずつ読込
ByteBuffer bufffer = ByteBuffer.allocateDirect(1);
上記のように「allocateDirect」メソッドを利用することで、ダイレクトバッファが利用できるようになるようです。