Solaris プラットフォームでのスレッド優先順位

このドキュメントでは、Java 仮想マシン (JVM) が、JVM (Java スレッド) で実行中のスレッドの優先順位を、Solaris のネイティブスレッドの優先順位へマッピングする方法について説明します。Solaris スレッドと JVM の現在と過去の両方の実装について説明します。

背景情報:Java スレッド

JVM では以下を含め、Java スレッドの 10 の論理的優先順位の範囲を定義します。

java.lang.Thread.MIN_PRIORITY  = 1
java.lang.Thread.NORM_PRIORITY = 5
java.lang.Thread.MAX_PRIORITY  = 10

これら [1..10] の値は Thread.setPriority(int) に渡され、Java スレッドに優先順位が割り当てられます。Java スレッドのデフォルトの優先順位は NORM_PRIORITY です (setPriority を明示的に呼び出さない Java スレッドは、NORM_PRIORITY で動作)。JVM では、この値を無視するなど、選択する任意の方法で優先順位を実装できます。

Java HotSpot 仮想マシンは現在、各 Java スレッドを一意のネイティブスレッドに関連付けます。Java スレッドとネイティブスレッドの関係は安定しており、Java スレッドの生存期間の間、永続します。

背景情報:Solaris

Libthread バリアント:T1 および T2

Solaris 9 より前では、デフォルトの libthread (スレッドライブラリ) は T1 libthread と呼ばれるものでした。T1 では M:N スレッドモデルを提供し、M 個のネイティブスレッド が N 個のカーネルスレッド (LWP) 上で多重化されました。ネイティブのスレッドと LWP との関係は流動的かつ動的であり、スレッドを実行している状態でも、またスレッドの関知なく変更されました。Solaris では、LWP のディスパッチ優先順位を変更するための priocntl() システムコールを提供しましたが、LWP とネイティブスレッドの関係が不安定だったため、ネイティブスレッドのディスパッチ優先順位を変更する確実な方法がありませんでした。(JVM では、Java スレッドを実行している LWP の優先順位を変更できましたが、JVM の関知なくスレッドが別の LWP に割り当てられることがありました。

Solaris 9 以降のデフォルトの libthread である T2 では、より簡単で強力な 1:1 スレッドモデルを実装しています。各ネイティブスレッドは、一意の LWP に割り当てられ、その関係はネイティブスレッドの生存期間中、安定しています。

T1 および T2 は、アプリケーションがスレッドのプロセスローカル優先順位の設定に使用する thr_setprio() API を公開しています。thr_setprio() から割り当てられる値は、プロセスローカル属性で、カーネルスケジューラには表示されません。thr_setprio() 優先順位は、競合するプロセスローカル相互排他ロックに関連するスレッドのキューなど、ユーザーレベルプロセスローカルスリープキューでスレッドの配置と順序付けを制御します。HotSpot では、ほとんどの相互排他ロックは競合しておらず、条件変数には通常、0 か 1 のスレッドがあります。したがって、thr_setprio() 優先順位による順序付けは、ほとんどの Java スレッドには影響を及ぼしません。thr_setprio() 関数は、0 から 127 までの優先順位の値をサポートし、127 は最高の優先順位を表します。

T1 もスレッドの優先順位を使用して、基本的なユーザーモード横取りを実装します。T1 では、ローカル実行可能状態キューのスレッドの thr_setprio() 優先順位は、現在実行中で LWP に関連付けられた未バインドスレッドの優先順位以下でなければならないという不変条件を保持します。この不変条件が危うくなる場合、T1 は、一番低い優先度で実行中スレッドを横取りして、不変条件を再確立するためにその LWP を「流用」します。

横取りは以下のような場合に発生できます。

T2 の初期のバージョンは、代替 libthreadと呼ばれ、Solaris 8 で導入されました。

T1、T2、および LWP の詳細は、以下のサイトを参照してください。

ネイティブ LWP の優先順位

Solaris の LWP の優先順位は、あるスレッドが受け取る CPU サイクル数に影響を及ぼします (ほかのスレッドとの相対数)。Solaris スケジューラは (ほかの要因の中で特に) 優先順位を使用して、あるスレッドがほかのスレッドを横取りするべきかどうか、スレッドを実行する頻度、およびスレッドの実行時間を決定します。ネイティブ LWP 優先順位は、priocntl() システムコールにより割り当てられます。

サマリー

要約すると、Java スレッドは、Thread.setPriority メソッドにより優先順位が設定されます。Java スレッドはネイティブスレッドで動作します。thr_setprio() 関数は、ネイティブスレッドの優先順位を変更するために使用されます。ネイティブスレッドは LWP で動作します。priocntl() システム呼び出しは LWP の優先順位を変更するために使用されます。

スレッド優先順位の実装の経緯

1.4.2 より前

1.4.2 より前のリリースでは、Java スレッドが Thread.setPriority メソッドを呼び出した場合、またはスレッドが作成された場合、HotSpot が thr_setprio() を呼び出して、Java の優先順位をネイティブ優先順位にマッピングしていました。thr_setprio() を呼び出しても、Java スレッドの実行動作にはほとんど影響がありませんでした。JVM は配下の LWP の優先順位を調整するために priocntl() を呼び出しませんでした。1.4.2 の開発期間中は、Solaris で唯一使用可能な libthread は以前の T1 の libthread だけであったため、このような設計上の選択が意識的になされました。

注: JVM では、スレッドの作成時に THR_BOUND を指定することで、ネイティブスレッドを T1 下の LWP に 1:1 でバインドさせることができました。ただし、JVM に接続されたスレッドが THR_BOUND でない可能性があり、原始スレッドが THR_BOUND ではないため、THR_BOUND は十分とはいえません。HotSpot 実装者は、Solaris にはスレッドの作成後にバインドさせる方法がないことを考慮し、Java スレッドが setPriority() を呼び出した場合に LWP 優先順位を変更しないほうが賢明だと判断しました。

1.4.2

1.4.2 では、HotSpot が起動時に T1 または T2 のどちらで実行しているかを判断できました。JVM が T1 下で起動した場合、優先順位の効果は前のリリースとまったく同じになります。

ただし T2 の場合は、1.4.2 は Thread.setPriority メソッドへの呼び出しを、thr_setprio() (ネイティブプロセスローカル優先順位を変更するため) と priocntl() (配下の LWP のディスパッチ優先順位を変更するため) の両方の呼び出しに変換しました。JVM は、TS (時分割)、IA (対話式)、および RT (リアルタイム) スケジューリングクラスで実行中のスレッドに対してのみ、priocntl() を呼び出しました。スケジューリングクラスの詳細については、Solaris priocntl(2) のマニュアルページを参照してください。Java スレッドが TS、IA、または、RT スケジューリングクラスにない場合、JVM は priocntl() で配下の LWP の優先順位を設定しません。

残念なことに、TS および IA スケジューリングクラス内のネイティブスレッドのデフォルト優先順位は、可能な範囲で最高の優先順位です。Java スレッドのデフォルト論理的優先順位は NORM_PRIORITY で、これは Java スレッド優先順位領域の中央です。JVM が NORM_PRIORITY をネイティブおよび LWP 優先順位にマッピングすると、結果の値はデフォルトネイティブ優先順位よりも低くなります。JVM を実行している 2 つの CPU システムがあり、JVM に 2 つの Java スレッドがあり、その両方が NORM_PRIORITY であるとします。よくあるように、スレッドが IA または TS スケジューリングクラスにあると想定します。Java スレッドが作成されると、JVM は priocntl() を呼び出して、NORM_PRIORITY を TS または IA 優先順位領域の中央にマッピングします。さらに、ほかのプロセスで 2 つのネイティブ「C」スレッドが Java スレッドと同時に実行中/実行可能状態だと想定します。ネイティブ C スレッドと Java スレッドは両方とも CPU に依存してスピンし、演算を行います。ネイティブスレッドは、TS および IA スケジューリングクラス内で最高の優先順位で実行され、JVM スレッドは中央の優先順位で実行されます。4 つのスレッドが CPU サイクルで競合しているため、ネイティブスレッドが比較的多くの CPU サイクルを受け取り、ある意味では Java スレッドが不利になります。こうした状況は、Java スレッドが通常のスレッドと競合して、システムが飽和状態のときに発生します。

逆に、相対的に低い優先順位を使用する利点は、TS および IA スケジューリングクラス内で低い優先順位で実行されているスレッドは、長いクォンタムを受け取ることです (実行可能状態になった優先度の高いスレッドが中央クォンタムで横取りしないと想定)。クォンタムが長いと、横取りからのコンテキスト切り替え率が下がるため、多くの場合、サーバーアプリケーションのスレッドが有利になります。スレッドは CPU で長い間動作することが許されるため、一時的にキャッシュの再ロードに時間がかかっても (スレッドが CPU にスケジュールされた直後の期間。スレッドが CPU のデータキャッシュを再生成して既存のスレッドデータを置き換えるため、スレッドのキャッシュ誤り率が高くなる)、長いクォンタムの間に償却されます。

JRE 5.0

JRE 5.0 は、1.4.2 と同じ優先順位マッピングを提供します。ただし、[10...5] 範囲のすべての Java 優先順位はできるだけ高い TS または IA 優先順位にマッピングされ、[1..4] 範囲の優先順位は低いネイティブ TS または IA 優先順位にマッピングされます。この変更による利点は、NORM_PRIORITY の Java スレッドが期待どおりネイティブスレッドと競合するようになったことです。よくあることですが、Java スレッドとネイティブスレッドのどちらも明示的に優先順位を設定しない場合、スレッドの両方のクラスが同じ占有スペースで競合し、TS または IA スケジューリングクラスのもっとも高い優先順位で実行されます。

Java スレッドが setPriority() を使用して明示的にその優先順位を設定しないことを想定して、この変更は 1.4.2 より前に使用されていた Java スレッドの動作と実効 LWP 優先順位を復元します。この実装の短所は、5 から 10 の Java 優先順位が区別されないことです。たとえば、論理的優先順位 8 の Java スレッドは、優先順位 9 の Java スレッドと同じ LWP 優先順位にマッピングされます。

その他

次の内容は、HotSpot のすべてのバージョンに当てはまります。

一般的なスケジューリング問題:優先順位、移譲、およびモニターの公平性

Thread.setPriority および Thread.yield メソッドは助言的なものです。この 2 つのメソッドはアプリケーションから JVM へのヒントを構成します。堅牢で、正しく記述された、プラットフォームに依存しないコードは、setPriority() および yield() を使用して、アプリケーションのパフォーマンスを最適化しますが、正確さについてはそれらの属性に依存するべきではありません。同様に、スレッドにモニターの所有権が付与される順序、または notifynotifyAll メソッドに応答してスレッドが再開する順序について、想定を行うべきではありません。このトピックについての詳しい説明は、Joshua Bloch の著書『Effective Java Programming Language Guide』の第 9 章、「Threads」を参照してください。


Copyright © 1993, 2013, Oracle and/or its affiliates. All rights reserved.