Javaのマルチスレッドのカスタム・クラス・ローダー

Java SE 7リリースには、マルチスレッドのカスタム・クラス・ローダーに関する重要な拡張機能が含まれています。以前のリリースでは、特定のタイプのカスタム・クラス・ローダーにデッドロックが発生する可能性がありました。Java SE 7リリースでは、デッドロックを回避するためにロック・メカニズムが変更されています。

背景

java.lang.ClassLoaderの機能は、特定のクラスのバイト・コードを見つけ、そのバイト・コードを実行時システムで使用可能なクラスに変換することです。実行時システムは、ブートストラップ・クラス、拡張クラス、およびユーザー・クラスを見つけることができるクラス・ローダーを提供します。CLASSPATH環境変数は、バイト・コードが置かれている場所を実行時システムに示すための1つの方法です。

クラスのロードについてはCLASSPATH環境変数を知るだけで十分です。ただし、状況によっては、ユーザー独自のサブクラスを作成してクラス・ローダーの動作をカスタマイズすることが必要になる場合があります。

カスタム・クラス・ローダーは、非循環式のクラス・ローダー委譲モデルに準拠していれば、デッドロックに陥ることはありません。ClassLoaderの設計では非循環式の委譲が想定されています。このモデルでは、各クラス・ローダーに親(委譲)が存在します。クラスが要求されたとき、クラス・ローダーはまず、そのクラスがすでにロードされているかどうかを確認します。クラスが見つからない場合、クラス・ローダーはその親にクラスを探すよう要求します。親がクラスを見つけることができない場合、クラス・ローダーはクラス自体を探すことを試みます。

デッドロックのシナリオ

Javaプラットフォームの以前のリリースでは、マルチスレッドのカスタム・クラス・ローダーが非循環式の委譲モデルに準拠していない場合、ローダーにデッドロックが発生する可能性がありました。次に例を示します。

Class Hierarchy:
  class A extends B
  class C extends D

ClassLoader Delegation Hierarchy:

Custom Classloader CL1:
  directly loads class A 
  delegates to custom ClassLoader CL2 for class B

Custom Classloader CL2:
  directly loads class C
  delegates to custom ClassLoader CL1 for class D

Thread 1:
  Use CL1 to load class A (locks CL1)
    defineClass A triggers
      loadClass B (try to lock CL2)

Thread 2:
  Use CL2 to load class C (locks CL2)
    defineClass C triggers
      loadClass D (try to lock CL1)

以前はClassLoaderクラスの同期はぞんざいで、技術用語でいうと粒度不足でした。ClassLoaderオブジェクト全体で同期されたクラスをロードするように要求すると、デッドロックが発生しやすくなりました。

Java SE 7リリースでのクラス・ローダーの同期

Java SE 7リリースには並行可能クラス・ローダーの概念が取り入れられています。現在は、並行可能クラス・ローダーでクラスをロードすると、クラス・ローダーとクラス名のペアに対して同期が行われます。

上記のシナリオで、Java SE 7リリースを使用すれば、スレッドのデッドロックは発生しなくなり、すべてのクラスが正常にロードされます。

Thread 1:
  Use CL1 to load class A (locks CL1+A)
    defineClass A triggers
      loadClass B (locks CL2+B)

Thread 2:
  Use CL2 to load class C (locks CL2+C)
    defineClass C triggers
      loadClass D (locks CL1+D)

マルチスレッドのカスタム・クラス・ローダーの推奨事項

デッドロックの履歴を持たないカスタム・クラス・ローダーには、変更の必要はありません。特に、推奨されている非循環式の階層型委譲モデル(つまり、まず親に委譲する)に従っているカスタム・クラス・ローダーは、変更する必要はありません。下位互換性のため、Java SE 7リリースでも、クラス・ローダー・オブジェクトは並行可能として登録されていないかぎりロックされます。

Java SE 7リリースで新しいカスタム・クラス・ローダーを作成するプロセスは、以前のリリースと同様です。ClassLoaderのサブクラスを作成してから、findClass()メソッドおよび必要に応じてloadClass()をオーバーライドします。loadClass()をオーバーライドすると取扱いが難しくなりますが、別の委譲モデルを使用するにはこの方法しかありません。

デッドロックの危険があるカスタム・クラス・ローダーを使用している場合、Java SE 7リリースでは、次の規則に従うことでデッドロックを回避できます。

  1. カスタム・クラス・ローダーがマルチスレッドに対して安全で、並行クラス・ロードに対応できることを確認します。
    1. 内部ロック方式を決定します。たとえば、java.lang.ClassLoaderは、要求されたクラス名に基づくロック方式を使用します。
    2. クラス・ローダー・オブジェクトのロックだけに対してすべての同期を削除します。
    3. 重要なセクションが、異なるクラスをロードする複数のスレッドに対して安全であることを確認します。
  2. カスタム・クラス・ローダーのstaticイニシャライザで、java.lang.ClassLoaderのstaticメソッドregisterAsParallelCapable()を呼び出します。この登録は、カスタム・クラス・ローダーのすべてのインスタンスがマルチスレッドに対して安全であることを示します。
  3. このカスタム・クラス・ローダーが拡張するすべてのクラス・ローダー・クラスも、そのクラス初期化子でregisterAsParallelCapable()メソッドを呼び出していることを確認します。これらがマルチスレッドに対して安全で、並行クラス・ロードに対応できることを確認します。

カスタム・クラス・ローダーでfindClass(String)だけをオーバーライドする場合は、これ以上の変更は必要ありません。カスタム・クラス・ローダーを作成するには、このメカニズムをお薦めします。

カスタム・クラス・ローダーでprotected loadClass(String, boolean)メソッドまたはpublic loadClass(String)メソッドのどちらかをオーバーライドする場合は、クラス・ローダーとクラス名のペアごとにprotectedメソッドdefineClass()が1回だけ呼び出されるようにする必要もあります。

トラブルシューティング

重要なセクションの処理が不完全なために、出荷した製品で問題が発生しているように見える場合は、新しいVMフラグ-XX:+AlwaysLockClassLoaderを使用できます。このフラグを使用すると以前のクラス・ローダー・ロック動作に戻り、並行可能として登録されているクラス・ローダーであっても、カスタム・クラス・ローダーのfindClass()またはloadClass()メソッドの呼出しの前にロックされます。

参照

OpenJDKの「Class Loader API Modifications for Deadlock Fix」

バグ・データベースのバグ4670071の詳細


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