JDK 1.1 開発ガイド (Solaris 編)

第 2 章 マルチスレッド

この章では、マルチスレッドの概要、および Solaris 上の Java に固有のマルチスレッド、ネイティブスレッド JVM について説明します。

Java を初めて使用する開発者のための情報には、タイトルにアスタリスク (*) が付いています。

マルチスレッドの定義 *

スレッドは、プロセス内の制御シーケンス (一連の制御の流れ) です。単一スレッドのプロセスは、1 つの制御シーケンスに従って動作します。マルチスレッドのプロセスは、複数の制御シーケンスを持ち、独立した複数の動作を同時に扱うことができます。複数のプロセッサが使用できる場合、このような独立したそれぞれの動作を実際に並列に実行することができます。

Solaris 環境における従来の Java スレッド *

Soiaris 2.6 以前の従来の Java 実行時環境では、Java 実行時スレッドおよびシステムサポート層の一部である、「グリーンスレッド」と呼ばれるスレッドライブラリが使用されていました。このグリーンスレッドライブラリはユーザーレベルのライブラリであり、Solaris システムは一度に 1 つのグリーンスレッドしか処理できません。このため Solaris は「複数対単一」のスレッド方式で Java 実行時環境を処理するので (「「複数対単一」のモデル (グリーンスレッド)」を参照)、次のような問題がありました。

アプリケーションのパフォーマンスを大幅に向上させるため、Solaris 2.6 プラットフォームの Java では、グリーンスレッドライブラリが Solaris のネイティブスレッドに置き換えられています。これは、Solaris 7 および Solaris 8 でも同様です。

マルチスレッドの概念 *

マルチスレッドプログラミングを行うことにより、ソフトウェア開発者は、アプリケーションを高速化したり、ハードウェアを並列的に動作させたりオブジェクトを有効活用することができます。Solaris のマルチスレッドの実装を利用するので、 Java は、効率的で信頼性が高く、標準に準拠し、開発者および一般ユーザーの両方に大きな利点をもたらします。マルチスレッドアプリケーションの開発において、Solaris オペレーティング環境は、最高のパフォーマンスとツール、サポート、柔軟性を提供します。Solaris オペレーティング環境には、次のような最新のマルチスレッド機能が採用されています。

マルチスレッド化の利点 *

マルチスレッド化の大きな利点の 1 つとして、並列動作によってアプリケーションの実行が高速になることが挙げられます。

マルチスレッド化によって、ハードウェアの並列性を最大限に活用し、さらにサブシステムであるマルチプロセッサを効果的に使用できます。マルチスレッドは、対称型マルチプロセッサを十分に活用するために欠かせないものです。また、マルチスレッド化によって、演算や入出力などの処理の重複が少なくなるので、シングルプロセッサのシステムでもパフォーマンスが向上します。

マルチスレッド化の大きな利点として、次のような点を挙げることができます。

マルチスレッドモデル

大部分のマルチスレッドモデルは、以下のいずれかの形態で実装されています。

「複数対単一」のモデル (グリーンスレッド)

「複数対単一」のモデル (1 つのカーネルスレッドに対してユーザースレッドが複数のモデル) では、アプリケーションは、並列に動作できるスレッドをいくつでも作成できます。スレッドのすべての動作はユーザー空間のみに限定されます。カーネルにアクセスできるスレッドは一度に 1 つだけなので、スケジューリング可能な 1 つのエンティティ (実体) だけがオペレーティングシステムに認識されます。このため、「複数対単一」のマルチスレッドモデルでの並列処理は限定されたものになり、マルチプロセッサが有効に利用されることはありません。Solaris システムにおける Java スレッドの初期の実装は、次の図に示すような「複数対単一」のモデルでした。

図 2-1 「複数対単一」のマルチスレッドモデル

Graphic

「単一対単一」のモデル

「単一対単一」のモデル (1 つのカーネルスレッドに対してユーザースレッドが 1 つのモデル) は、正しいマルチスレッドの初期の実装の 1 つです。「単一対単一」のモデルでは、アプリケーションによって作成されたユーザーレベルのスレッドはそれぞれカーネルによって認識され、すべてのスレッドが同時にカーネルにアクセスできます。「単一対単一」モデルの最大の問題は、スレッドが増えるほどプロセスが重くなるため、開発者はそのことを考慮しながらスレッドをあまり多く使用しないようにする必要があることです。このため、Windows NT や OS/2 のスレッドパッケージなどの「単一対単一」モデルのスレッド実装の多くは、システムでサポートされるスレッド数を制限しています。

図 2-2 「単一対単一」のマルチスレッドモデル

Graphic

「複数対複数」のモデル (Solaris 上の Java - ネイティブスレッド)

「複数対複数」のモデル (複数のカーネルスレッドに対してユーザースレッドが複数のモデル) は、「単一対単一」のモデルが持つ制限の多くを解消し、マルチスレッドの応用範囲を広げます。2 レベルモデルとも呼ばれる「複数対複数」のモデルは、各スレッドの負荷を軽減し、プログラミング作業も簡潔になります。

「複数対複数」のモデルでは、プログラムは、プロセスを重くしすぎることなく適切な個数のスレッドを持つことができます。ユーザーレベルのスレッドライブラリによって、カーネルスレッドの上位でユーザーレベルのスレッドをスケジューリングすることが可能になります。カーネルが管理する必要があるのは、アクティブになっているスレッドだけです。ユーザーレベルで「複数対複数」のモデルが実装されることにより、アプリケーションで効果的に使用できるスレッド数の制限がなくなるため、プログラミング作業が軽減されます。

つまり、標準のインタフェースを持つより簡単なプログラミングモデルが提供され、すべてのプロセスについて最高のパフォーマンスが得られるようになります。Solaris 上の Java オペレーティング環境は、市販製品で初めてマルチスレッドオペレーティングシステムに「複数対複数」モデルの Java が実装された環境です。

図 2-3 「複数対複数」のマルチスレッドモデル

Graphic

マルチスレッドカーネル

マルチスレッドカーネルは、マルチスレッドの完全な実装の基礎となるものです。Solaris オペレーティングシステムで使用されているようなマルチスレッドカーネルでは、各カーネルスレッドはカーネルのアドレス空間内の 1 つの制御の流れです。カーネルスレッドは完全にプリエンプティブであり、システムで使用できるリアルタイムクラスなど任意のスケジューリングクラスを使用して、スケジューリングすることができます。あらゆる実行エンティティは、カーネルスレッドを使用して作成されます。カーネルスレッドは、カーネル内で完全にプリエンプティブな、リアルタイムの「核」とみなすことができます。

カーネルスレッドは、スレッドやプロセスが別の処理が完了するのを待っていたために優先順位どおりに実行されなくなるのを防ぐためのプロトコルをサポートする、同期プリミティブを使用します。これにより、アプリケーションは意図したとおりに動作するようになります。カーネルスレッドはまた、NFS デーモン、ページアウトデーモン、割り込み、などのカーネルレベルのタスクが非同期に動作することを可能にして、並列処理性および全体のスループットを向上させます。

マルチスレッドカーネルは、一般的な JVM 実装などのマルチスレッドアプリケーションアーキテクチャの作成に欠かせないものです。マルチスレッドカーネルには、次の特徴があります。

Solaris 上の Java マルチスレッドの利点

Solaris のマルチスレッドカーネルは、Solaris オペレーティングシステムの特に重要な構成要素の 1 つです。このマルチスレッドカーネルによって Solaris オペレーティングシステムは、効率的な並列処理を実現し、高機能を備えている、唯一の標準的なオペレーティングシステムになっています。

Solaris 上の Java は、カーネルのマルチスレッド機能を利用しています。開発者は、単純なプログラミングインタフェースを使用して、マルチプロセッサあるいはシングルプロセッサシステム用の、数千のユーザーレベルのスレッドを持つ Java アプリケーションを作成することができます。

Solaris 上の Java 環境は、「複数対複数」のスレッドモデルをサポートします。図 2-4 に示すように、Solaris の 2 レベルアーキテクチャは、軽量プロセス (LWP) と呼ばれる中間層を提供することによって、プログラミングインタフェースと実装とを分離します。LWP が提供されることにより、アプリケーション開発者は、汎用のアプリケーションレベルのインターフェースを使用して、高速で負荷が軽いスレッドを短期間に作成できます。スレッドを利用するようにアプリケーションを作成しておけば、実行時環境側でスレッドライブラリの実装に基づいて実行可能なスレッドを実行時資源 (LWP) に多重化し、スケジューリングしてくれます。

各 LWP は、コードやシステムコールを実行する仮想の CPU のような動作をします。LWP は、スケジューリングクラスと優先順位に従って、カーネルによって個々にディスパッチされる (振り分けられる) ため、独立にシステムコールを行なったり、独立にページフォルトを発生させたり、複数のプロセッサ上で並列的に動作したりすることができます。スレッドライブラリは、システムのスケジューラとは別のユーザーレベルのスケジューを提供します。ユーザーレベルのスレッドは、カーネルのスケジューリングが可能な LWP によってカーネル内でサポートされます。カーネルの LWP プール上に、多数のユーザースレッドが多重化されます。

図 2-4 Solaris の 2 レベルアーキテクチャ

Graphic

Solaris スレッドでは、ユーザーレベルのスレッドを LWP に結合するかあるいは LWP に結合しないままにするかを、アプリケーションが選択することができます。ユーザーレベルのスレッドと LWP とは、排他的に結合されます。スレッドの結合は、リアルタイムの応答を必要とするアプリケーションなど、自身の多重性 (並列処理) を厳密に制御する必要があるアプリケーションにとっては便利です。スレッド結合を行う Java API はありません。大部分の Java アプリケーションでは、スレッド結合は必要ないと考えられるためです。結合が必要な場合は、Solaris のネイティブメソッド呼び出しを行なって結合することができます。

このため、すべての Java スレッドはデフォルトでは非結合状態になっています。ユーザーレベルの非結合スレッドの並列処理は、スレッドライブラリが制御します。スレッドライブラリは、アプリケーションが必要とする非結合スレッドに合わせて LWP プールを大きくしたり小さくしたりします。

Solaris 環境のすべての Java アプリケーションで、Solaris 上の Java スレッドに固有の次の機能をデフォルトで使用できます。


注 -

一般的に、ネイティブスレッドを使用する Solaris のネイティブ機能を、Java アプリケーションが使用しないようにしてください。Java アプリケーションが Solaris プラットフォーム用に限定されてしまい、他のプラットフォームで動作しなくなる可能性があります。また、100% Pure JavaTM にも準拠しなくなります。


Java アプリケーションから Solaris 固有の機能にアクセスすることは推奨できませんが、次に Solaris のマルチスレッドアーキテクチャの豊富な機能の例を紹介します。

Solaris の 2 レベルモデルでは、多数の異なるプログラミング要件を満たすことができるよう、従来では見られないような高い柔軟性が提供されます。ウィンドウプログラムのように (少なくとも) 見かけ上は多くの要求を並列に処理する必要があるプログラもあれば、行列の乗算を行うプログラムのように並列演算を実際に使用可能な数のプロセッサに割り当てる必要のあるプログラムもあります。2 レベルモデルであることによって、カーネルは、システムサービスに対するスレッドからのアクセスをブロックしたり制限したりせずに、あらゆる種類のプログラムの並列処理要求に応えることができます。

Solaris 上の Java は、システム資源を効率的に使用するように設計されています。スレッドを使用することによるオーバーヘッドを最低限に抑えながら、アプリケーションに数千のスレッドを持たせることができます。スレッドはそれぞれ独立して動作し、同じプロセス中の他のスレッドとプロセス命令を共有したり、データを透過的に共有したりします。またスレッドは、プロセスのオペレーティングシステム状態の多くを共有するので、あるスレッドで開いたファイルを他のスレッドから読み込むこともできます。また程度の差はありますが、別々のプロセス間の同期をとることもできます。

Solaris のスレッドモデルに基づく Java は、速度、多重性、機能、カーネル資源の利用の面で、最良の組み合わせを提供します。

スレッドのグループ化

Java スレッドそれぞれは、スレッドグループを構成するメンバーです。スレッドグループは、複数のスレッドを別々に操作するのではなく、複数のスレッドを 1 つのオブジェクトに集めることによって、それらのスレッドをすべて一度にまとめて操作するためのものです。

たとえば、1 回のスレッド呼び出しによって、1 つのグループ内のすべてのスレッドを開始したり一時停止したりすることができます。Java スレッドグループは、java.lang パッケージ中の ThreadGroup クラスによって実装されています。実行時システムは、スレッドの構築中にそのスレッドをスレッドグループに追加します。スレッドを作成するときに、その新しいスレッドを適切なデフォルトのスレッドグループに追加するか、または明示的に新しいスレッドグループを指定することもできます。スレッドの作成時にいったんスレッドをスレッドグループに追加したら、そのスレッドを別のスレッドグループへ移動することはできません。

Java スレッドに関する注意事項

Solaris 用の Java アプリケーションを作成する場合の、Java 全般および Solaris に固有の Java スレッドに関する注意事項を説明します。

Java 全般の注意事項

JDK 1.1 で推奨されないメソッドについては、表 4-1を参照してください。

Solaris に固有の問題

以降で説明するように、いくつかの問題は Solaris に固有です。

マルチスレッドで安全でないライブラリの使用


注意 - 注意 -

他に回避方法がない場合のみ、以下の方法を使用してください。正規の方法ではないので、十分に注意してプログラミングを行わないと、デッドロックが発生する可能性があります。


-D_REENTRANT を有効にせずにコンパイルされた既存のライブラリを使用する Java アプリケーションを実行しようとすると、以下のような問題が発生します。

JDK 1.1 のようなネイティブスレッドの JVM の場合、libc は、システムコールのエラーコードをスレッド固有の errno に書き込みます。-D REENTRANT フラグを有効にしてコンパイルされていないため、マルチスレッドで安全でないライブラリは、errno を参照するときにグローバル変数の errno を参照します。このため、ライブラリはスレッド固有の errno にアクセスすることができず、失敗したシステムコールに対応する正しい応答を返すことができません。

この問題を根本的に解決するには、ネイティブメソッドによってネイティブコードを使用するマルチスレッドの Java アプリケーションが、マルチスレッドで安全 (または少なくとも errno が安全) なライブラリとリンクされるようにするようにします。

errno が安全でないライブラリをどうしても参照する必要がある場合は、次のような回避方法があります。Java アプリケーションをメインスレッドで開始し、すべての安全でないライブラリに対する呼び出しがメインスレッド経由で行われるようにします。たとえばスレッドが JNI を呼び出す場合、JVM を使って、メインスレッドによって処理される 1 つの待ち行列に、すべての JNI 引数を整列化して追加することができます。このようにして、Java ネイティブインタフェース (JNI、Java Native Interface) を呼び出すスレッドは、自分の代わりにメインスレッドが呼び出しを実行してその結果を返してくれるのを待ちます。

安全でないライブラリに対する呼び出しがメインスレッドからのみ実行される時には、ライブラリ中でも単一スレッドによって処理が行われるので、ロックによって排他処理を行う必要はありません。メインスレッドである程度の並列処理を実現するために、ブロックされない (入出力処理の完了を待たずに復帰する) 呼び出しを実行する場合もありますが、メインスレッドから参照されるのはグローバル変数の errno なので、libc とマルチスレッドで安全でないライブラリの両方も同じ errno を参照することになります。

interrupt() メソッド

このメソッドには現在のところ特別に便利な機能はないので、一般的には推奨していません。Java 言語仕様 (JLS、Java Language Specification) では、対象のスレッドが wait() メソッドを呼び出しているときだけその対象スレッドに割り込む、と定義されています。

Solaris プラットフォームでは、対象のスレッドが入出力の呼び出しを行なっている時にも割り込むように、このメソッドの動作が拡張されていますが、interrupt() メソッドのこの動作に依存しないようにしてください。この拡張された動作は将来サポートされなくなる可能性があり、また異なる JVM 間でコードの互換性がなくなるためです。

スレッドの優先順位

ネイティブスレッド化された JVM での Java スレッドでは、スレッドに優先順位を付けることができますが、スケジューラはこの値を単なるヒントとして扱います。特に、演算処理が中心のスレッドがある場合には、スレッドがその優先順位どおりに実行されない可能性があります。通常、1 つのプロセスについて利用できる複数のプロセッサは動的で予測できないため、スレッドに優先順位を付けることによって、マルチタスクのマルチプロセッサシステム上での処理をスケジューリングしようとしても、あまりうまくいきません。