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

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 の詳細

「Internals of Java Class Loading」、Binildas Christudas 著、O'Reilly Media onJava.com

「Demystifying class loading problems, Part 4: Deadlocks and constraints」、Simon Burns および Lakshmi Shankar 共著、IBM developerWorks


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