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で導入されました。

ネイティブ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メソッドに応答してスレッドが再開する順序について、想定を行うべきではありません。


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