web-dev-qa-db-ja.com

非スレッドセーフライブラリを並行して実行したい-複数のクラスローダーを使用して実行できますか?

私は、スレッドセーフが保証されていない(そしてそうではない)ライブラリを使用し、Java 8ストリームシナリオでシングルスレッド化されているプロジェクトに取り組んでいます。これは、期待どおりに機能します。

並列ストリームを使用して、拡張性の低い成果を実現したいと考えています。

残念ながら、これによりライブラリが失敗します(おそらく、一方のインスタンスが他方のインスタンスと共有されている変数に干渉するためです)。したがって、分離が必要です。

インスタンスごとに個別のクラスローダー(おそらくスレッドローカル)を使用することを検討していました。これは、私の知る限り、すべての実用的な目的で必要な分離を取得することを意味しますが、この目的でクラスローダーを意図的に構築することに慣れていません。

これは正しいアプローチですか?適切な生産品質を得るためにこれをどのように行うのですか?


編集:私はそれをよりよく理解するために、質問を引き起こした状況についての追加情報を求められました。問題はまだ一般的な状況についてであり、ライブラリを修正することではありません。

ライブラリによって作成されたオブジェクト( https://github.com/veraPDF/ )によってプルされたオブジェクトを完全に制御できます。

<dependency>
    <groupId>org.verapdf</groupId>
    <artifactId>validation-model</artifactId>
    <version>1.1.6</version>
</dependency>

アーティファクトにプロジェクトMavenリポジトリを使用する。

<repositories>
    <repository>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
        <id>vera-dev</id>
        <name>Vera development</name>
        <url>http://artifactory.openpreservation.org/artifactory/vera-dev</url>
    </repository>
</repositories>

今のところ、ライブラリを強化することは不可能です。


編集:私はコードを表示するように頼まれました。私たちのコアアダプターは大まかに:

public class VeraPDFValidator implements Function<InputStream, byte[]> {
    private String flavorId;
    private Boolean prettyXml;

    public VeraPDFValidator(String flavorId, Boolean prettyXml) {
        this.flavorId = flavorId;
        this.prettyXml = prettyXml;
        VeraGreenfieldFoundryProvider.initialise();
    }

    @Override
    public byte[] apply(InputStream inputStream) {
        try {
            return apply0(inputStream);
        } catch (RuntimeException e) {
            throw e;
        } catch (ModelParsingException | ValidationException | JAXBException | EncryptedPdfException e) {
            throw new RuntimeException("invoking VeraPDF validation", e);
        }
    }

    private byte[] apply0(InputStream inputStream) throws ModelParsingException, ValidationException, JAXBException, EncryptedPdfException {
        PDFAFlavour flavour = PDFAFlavour.byFlavourId(flavorId);
        PDFAValidator validator = Foundries.defaultInstance().createValidator(flavour, false);
        PDFAParser loader = Foundries.defaultInstance().createParser(inputStream, flavour);
        ValidationResult result = validator.validate(loader);

        // do in-memory generation of XML byte array - as we need to pass it to Fedora we need it to fit in memory anyway.

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        XmlSerialiser.toXml(result, baos, prettyXml, false);
        final byte[] byteArray = baos.toByteArray();
        return byteArray;
    }
}

これは、InputStream(PDFファイルを提供)からバイト配列(XMLレポート出力を表す)にマップする関数です。

(コードを見ると、コンストラクターでイニシャライザーが呼び出されていることに気付きました。これが私の特定のケースの原因である可能性があります。それでも、一般的な問題の解決策が必要です。

私たちは同様の課題に直面しています。問題は通常、さまざまなスレッド間で不本意に「共有」された静的プロパティから発生しました。

クラスローダーによってロードされたクラスに静的プロパティが実際に設定されていることを保証できる限り、さまざまなクラスローダーを使用することができました。 Javamayには、スレッド間で分離されていない、またはスレッドセーフではないプロパティまたはメソッドを提供するクラスがいくつかあります( 'System.setProperties()Security.addProvider()はOKです-この問題に関する標準的なドキュメントは歓迎されます)。

潜在的に実行可能で高速なソリューション(少なくともライブラリでこの理論をテストする機会を与えることができる)は、JettyやTomcatなどのサーブレットエンジンを使用することです。

ライブラリを含むいくつかの戦争を構築し、プロセスを並行して開始します(戦争ごとに1つ)。

サーブレットスレッド内でコードを実行する場合、これらのエンジンのWebappClassLoadersは、最初に親クラスローダー(エンジンと同じ)からクラスをロードしようとし、クラスが見つからない場合は、からクラスをロードしようとします。戦争でパッケージ化されたjar /クラス。

Jettyを使用すると、選択したコンテキストに合わせてプログラムでウォーをホットデプロイし、必要に応じてプロセッサ(ウォー)の数を理論的にスケーリングできます。

URLClassLoaderを拡張して独自のクラスローダーを実装し、Jetty WebappClassLoaderからインスピレーションを得ました。見た目ほど難しい仕事ではありません。

私たちのクラスローダーは正反対のことをします:それは 'パッケージ'firstにローカルなjarからクラスをロードしようとし、次に親からそれらを取得しようとしますクラスローダー。これにより、親クラスローダーによって誤ってロードされたライブラリが(最初に)考慮されないことが保証されます。私たちの「パッケージ」は、実際には、カスタマイズされたマニフェストファイルを持つ他のjar /ライブラリを含むjarです。

このクラスローダーコードを「現状のまま」投稿しても、あまり意味がありません(そして、いくつかの著作権の問題が発生します)。そのルートをさらに探索したい場合は、スケルトンを考え出すことができます。

Jetty WebappClassLoader のソース

13
Bruno Grieder

答えは、実際にはライブラリが何に依存しているかによって異なります。

  1. ライブラリが少なくとも1つのネイティブライブラリに依存している場合、ClassLoadersを使用してライブラリのコードを分離しても、 JNI仕様 によると、同じJNIネイティブライブラリを複数にロードすることは許可されていないため、役に立ちません。最終的にUnsatisfiedLinkErrorになるような1つのクラスローダー。
  2. ライブラリが、たとえばファイルのように共有されることを意図しておらず、ライブラリによって変更された少なくとも1つの外部リソースに依存している場合、複雑なバグやリソースの破損が発生する可能性があります。

上記のケースに当てはまらないと仮定すると、一般的に、クラスが非スレッドセーフとして知られており、静的フィールドを変更しない場合は、呼び出しごとまたはスレッドごとにこのクラスの専用インスタンスを使用するだけで、クラスインスタンスとして十分です。その後、共有されなくなります。

ここで、ライブラリは明らかに共有されることを意図していないいくつかの静的フィールドに依存して変更するため、ライブラリのクラスを専用のClassLoaderに分離する必要があります。もちろん、スレッドが同じClassLoaderを共有しないようにしてください。

このためには、ライブラリの場所をURLClassLoaderとして指定する URL を作成するだけで済みます( URLClassLoader.newInstance(URL[] urls, ClassLoader parent) を使用)。エントリポイントに対応するライブラリのクラスを取得し、ターゲットメソッドを呼び出します。呼び出しごとに新しいURLClassLoaderが作成されないようにするには、 ThreadLocal を使用して、特定のスレッドに使用されるURLClassLoaderまたはClassまたはMethodインスタンスを格納することを検討できます。


だからここにあなたが進むことができる方法があります:

私のライブラリのエントリポイントが次のようなクラスFooであるとしましょう。

_package com.company;

public class Foo {

    // A static field in which we store the name of the current thread
    public static String threadName;

    public void execute() {
        // We print the value of the field before setting a value
        System.out.printf(
            "%s: The value before %s%n", Thread.currentThread().getName(), threadName
        );
        // We set a new value
        threadName = Thread.currentThread().getName();
        // We print the value of the field after setting a value
        System.out.printf(
            "%s: The value after %s%n", Thread.currentThread().getName(), threadName
        );
    }
}
_

このクラスは明らかにスレッドセーフではなく、メソッドexecuteは、ユースケースのように並行スレッドによって変更されることを意図していない静的フィールドの値を変更します。

ライブラリを起動するには、Fooのインスタンスを作成し、メソッドexecuteを呼び出す必要があると仮定します。対応するMethodThreadLocalに格納して、次のように ThreadLocal.withInitial(Supplier<? extends S> supplier) を使用して、スレッドごとに1回だけリフレクションによって取得できます。

_private static final ThreadLocal<Method> TL = ThreadLocal.withInitial(
    () -> {
        try {
            // Create the instance of URLClassLoader using the context 
            // CL as parent CL to be able to retrieve the potential 
            // dependencies of your library assuming that they are
            // thread safe otherwise you will need to provide their 
            // URL to isolate them too
            URLClassLoader cl = URLClassLoader.newInstance(
                new URL[]{/* Here the URL of my library*/},
                Thread.currentThread().getContextClassLoader()
            );
            // Get by reflection the class Foo
            Class<?> myClass = cl.loadClass("com.company.Foo");
            // Get by reflection the method execute
            return myClass.getMethod("execute");
        } catch (Exception e) {
            // Here deal with the exceptions
            throw new IllegalStateException(e);
        }
    }
);
_

そして最後に、私のライブラリの同時実行をシミュレートしましょう。

_// Launch 50 times concurrently my library
IntStream.rangeClosed(1, 50).parallel().forEach(
    i -> {
        try {
            // Get the method instance from the ThreadLocal
            Method myMethod = TL.get();
            // Create an instance of my class using the default constructor
            Object myInstance = myMethod.getDeclaringClass().newInstance();
            // Invoke the method
            myMethod.invoke(myInstance);
        } catch (Exception e) {
            // Here deal with the exceptions
            throw new IllegalStateException(e);
        }
    }
);
_

スレッド間に競合がなく、スレッドがexecuteの呼び出しから別の呼び出しに対応するクラス/フィールドの値を適切に再利用することを示す次のタイプの出力が得られます。

_ForkJoinPool.commonPool-worker-7: The value before null
ForkJoinPool.commonPool-worker-7: The value after ForkJoinPool.commonPool-worker-7
ForkJoinPool.commonPool-worker-7: The value before ForkJoinPool.commonPool-worker-7
ForkJoinPool.commonPool-worker-7: The value after ForkJoinPool.commonPool-worker-7
main: The value before null
main: The value after main
main: The value before main
main: The value after main
...
_

このアプローチではスレッドごとに1つのClassLoaderが作成されるため、スレッド数が固定されたスレッドプールを使用してこのアプローチを適用してください。また、ClassLoaderはメモリフットプリントなので、ヒープサイズに応じてインスタンスの総数を制限する必要があります。

ライブラリの使用が終了したら、スレッドプールの各スレッドのThreadLocalをクリーンアップして、メモリリークを防止する必要があります。これを行うには、次の手順を実行します。

_// The size of your the thread pool
// Here as I used for my example the common pool, its size by default is
// Runtime.getRuntime().availableProcessors()
int poolSize = Runtime.getRuntime().availableProcessors();
// The cyclic barrier used to make sure that all the threads of the pool
// will execute the code that will cleanup the ThreadLocal
CyclicBarrier barrier = new CyclicBarrier(poolSize);
// Launch one cleanup task per thread in the pool
IntStream.rangeClosed(1, poolSize).parallel().forEach(
    i -> {
        try {
            // Wait for all other threads of the pool
            // This is needed to fill up the thread pool in order to make sure 
            // that all threads will execute the cleanup code
            barrier.await();
            // Close the URLClassLoader to prevent memory leaks
            ((URLClassLoader) TL.get().getDeclaringClass().getClassLoader()).close();
        } catch (Exception e) {
            // Here deal with the exceptions
            throw new IllegalStateException(e);
        } finally {
            // Remove the URLClassLoader instance for this thread
            TL.remove();
        }
    }
);
_
8
Nicolas Filotto

私は質問が興味深いことに気づき、あなたのために小さなツールを作成しました:

https://github.com/kriegaex/ThreadSafeClassLoader

現在、Maven Centralの公式リリースとしてはまだ利用できませんが、次のようなスナップショットを取得できます。

_<dependency>
  <groupId>de.scrum-master</groupId>
  <artifactId>threadsafe-classloader</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>

<!-- (...) -->

<repositories>
  <repository>
    <snapshots>
      <enabled>true</enabled>
    </snapshots>
    <id>ossrh</id>
    <name>Sonatype OSS Snapshots</name>
    <url>https://oss.sonatype.org/content/repositories/snapshots</url>
  </repository>
</repositories>
_

クラス ThreadSafeClassLoader

このスレッドの他の部分で説明されているクラスのロード、オブジェクトのインスタンス化、およびプロキシ生成機能をすでに提供しているため、内部で JCL(Jar Class Loader) を使用します。 (なぜ車輪を再発明するのですか?)私が一番上に追加したのは、まさにここで必要なもののための素晴らしいインターフェースです:

_package de.scrum_master.thread_safe;

import org.xeustechnologies.jcl.JarClassLoader;
import org.xeustechnologies.jcl.JclObjectFactory;
import org.xeustechnologies.jcl.JclUtils;
import org.xeustechnologies.jcl.proxy.CglibProxyProvider;
import org.xeustechnologies.jcl.proxy.ProxyProviderFactory;

import Java.util.ArrayList;
import Java.util.Arrays;
import Java.util.List;

public class ThreadSafeClassLoader extends JarClassLoader {
  private static final JclObjectFactory OBJECT_FACTORY = JclObjectFactory.getInstance();

  static {
    ProxyProviderFactory.setDefaultProxyProvider(new CglibProxyProvider());
  }

  private final List<Class> classes = new ArrayList<>();

  public static ThreadLocal<ThreadSafeClassLoader> create(Class... classes) {
    return ThreadLocal.withInitial(
      () -> new ThreadSafeClassLoader(classes)
    );
  }

  private ThreadSafeClassLoader(Class... classes) {
    super();
    this.classes.addAll(Arrays.asList(classes));
    for (Class clazz : classes)
      add(clazz.getProtectionDomain().getCodeSource().getLocation());
  }

  public <T> T newObject(ObjectConstructionRules rules) {
    rules.validate(classes);
    Class<T> castTo = rules.targetType;
    return JclUtils.cast(createObject(rules), castTo, castTo.getClassLoader());
  }

  private Object createObject(ObjectConstructionRules rules) {
    String className = rules.implementingType.getName();
    String factoryMethod = rules.factoryMethod;
    Object[] arguments = rules.arguments;
    Class[] argumentTypes = rules.argumentTypes;
    if (factoryMethod == null) {
      if (argumentTypes == null)
        return OBJECT_FACTORY.create(this, className, arguments);
      else
        return OBJECT_FACTORY.create(this, className, arguments, argumentTypes);
    } else {
      if (argumentTypes == null)
        return OBJECT_FACTORY.create(this, className, factoryMethod, arguments);
      else
        return OBJECT_FACTORY.create(this, className, factoryMethod, arguments, argumentTypes);
    }
  }

  public static class ObjectConstructionRules {
    private Class targetType;
    private Class implementingType;
    private String factoryMethod;
    private Object[] arguments;
    private Class[] argumentTypes;

    private ObjectConstructionRules(Class targetType) {
      this.targetType = targetType;
    }

    public static ObjectConstructionRules forTargetType(Class targetType) {
      return new ObjectConstructionRules(targetType);
    }

    public ObjectConstructionRules implementingType(Class implementingType) {
      this.implementingType = implementingType;
      return this;
    }

    public ObjectConstructionRules factoryMethod(String factoryMethod) {
      this.factoryMethod = factoryMethod;
      return this;
    }

    public ObjectConstructionRules arguments(Object... arguments) {
      this.arguments = arguments;
      return this;
    }

    public ObjectConstructionRules argumentTypes(Class... argumentTypes) {
      this.argumentTypes = argumentTypes;
      return this;
    }

    private void validate(List<Class> classes) {
      if (implementingType == null)
        implementingType = targetType;
      if (!classes.contains(implementingType))
        throw new IllegalArgumentException(
          "Class " + implementingType.getName() + " is not protected by this thread-safe classloader"
        );
    }
  }
}
_

私はいくつかの ユニット および 統合 テストで私の概念をテストしました。そのうちの1つは veraPDF問題を再現して解決する の方法を示しています。

これが、私の特別なクラスローダーを使用したときのコードの外観です。

クラス VeraPDFValidator

クラスに_static ThreadLocal<ThreadSafeClassLoader>_メンバーを追加し、新しいクラスローダーに配置するクラス/ライブラリを指定します(ライブラリごとに1つのクラスについて言及するだけで十分です。その後、ツールがライブラリを自動的に識別します)。

次に、threadSafeClassLoader.get().newObject(forTargetType(VeraPDFValidatorHelper.class))を介して、スレッドセーフなクラスローダー内でヘルパークラスをインスタンス化し、そのプロキシオブジェクトを作成して、外部から呼び出すことができるようにします。

ところで、_static boolean threadSafeMode_は、veraPDFの古い(安全でない)使用法と新しい(スレッドセーフな)使用法を切り替えて、元の問題をネガティブ統合テストケースで再現できるようにするためにのみ存在します。

_package de.scrum_master.app;

import de.scrum_master.thread_safe.ThreadSafeClassLoader;
import org.verapdf.core.*;
import org.verapdf.pdfa.*;

import javax.xml.bind.JAXBException;
import Java.io.InputStream;
import Java.lang.reflect.InvocationTargetException;
import Java.util.function.Function;

import static de.scrum_master.thread_safe.ThreadSafeClassLoader.ObjectConstructionRules.forTargetType;

public class VeraPDFValidator implements Function<InputStream, byte[]> {
  public static boolean threadSafeMode = true;

  private static ThreadLocal<ThreadSafeClassLoader> threadSafeClassLoader =
    ThreadSafeClassLoader.create(           // Add one class per artifact for thread-safe classloader:
      VeraPDFValidatorHelper.class,         //   - our own helper class
      PDFAParser.class,                     //   - veraPDF core
      VeraGreenfieldFoundryProvider.class   //   - veraPDF validation-model
    );

  private String flavorId;
  private Boolean prettyXml;

  public VeraPDFValidator(String flavorId, Boolean prettyXml)
    throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    this.flavorId = flavorId;
    this.prettyXml = prettyXml;
  }

  @Override
  public byte[] apply(InputStream inputStream) {
    try {
      VeraPDFValidatorHelper validatorHelper = threadSafeMode
        ? threadSafeClassLoader.get().newObject(forTargetType(VeraPDFValidatorHelper.class))
        : new VeraPDFValidatorHelper();
      return validatorHelper.validatePDF(inputStream, flavorId, prettyXml);
    } catch (ModelParsingException | ValidationException | JAXBException | EncryptedPdfException e) {
      throw new RuntimeException("invoking veraPDF validation", e);
    }
  }
}
_

クラス VeraPDFValidatorHelper

このクラスでは、壊れたライブラリへのすべてのアクセスを分離します。ここでは特別なことは何もありません。OPの質問からコピーされたコードだけです。ここで行われることはすべて、スレッドセーフなクラスローダー内で行われます。

_package de.scrum_master.app;

import org.verapdf.core.*;
import org.verapdf.pdfa.*;
import org.verapdf.pdfa.flavours.PDFAFlavour;
import org.verapdf.pdfa.results.ValidationResult;

import javax.xml.bind.JAXBException;
import Java.io.ByteArrayOutputStream;
import Java.io.InputStream;

public class VeraPDFValidatorHelper {
  public byte[] validatePDF(InputStream inputStream, String flavorId, Boolean prettyXml)
    throws ModelParsingException, ValidationException, JAXBException, EncryptedPdfException
  {
    VeraGreenfieldFoundryProvider.initialise();
    PDFAFlavour flavour = PDFAFlavour.byFlavourId(flavorId);
    PDFAValidator validator = Foundries.defaultInstance().createValidator(flavour, false);
    PDFAParser loader = Foundries.defaultInstance().createParser(inputStream, flavour);
    ValidationResult result = validator.validate(loader);

    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    XmlSerialiser.toXml(result, baos, prettyXml, false);
    return baos.toByteArray();
  }
}
_
7
kriegaex

スレッドごとにクラスローダーでライブラリを分離することにより、提案したとおりにクラスの同時実行プロパティを保証できます。唯一の例外は、bootstrapクラスローダーまたはシステムクラスローダーと明示的に相互作用するライブラリです。リフレクションまたはInstrumentation AP​​Iのいずれかによって、これらのクラスローダーにクラスを挿入できます。 。そのような機能の1つの例は、Mockitoのインラインモックメーカーですが、私の知る限り、同時実行の制約はありません。

この動作でクラスローダーを実装することは、それほど難しいことではありません。最も簡単な解決策は、プロジェクトに必要なjarを明示的に含めることです。リソースとして。このように、クラスをロードするためにURLClassLoaderを使用できます。

URL url = getClass().getClassLoader().getResource("validation-model-1.1.6.jar");
ClassLoader classLoader = new URLClassLoader(new URL[] {url}, null);

nullURLClassLoader(2番目の引数)のスーパークラスローダーとして参照することにより、bootstrapクラスの外部に共有クラスがないことを保証します。注この作成されたクラスローダーのクラスを外部から使用することはできません。ただし、ロジックをトリガーするクラスを含む2番目のjarを追加すると、リフレクションなしでアクセスできるエントリポイントを提供できます。

class MyEntryPoint implements Callable<File> {
  @Override public File call() {
    // use library code.
  }
}

このクラスを独自のjarに追加し、2番目の要素として上記のURL配列に指定するだけです。ライブラリタイプは、エントリポイントを使用するクラスローダーの外部に住むコンシューマーでは使用できないため、戻り値として参照できないことに注意してください。

クラスローダーの作成をThreadLocalにラップすることで、クラスローダーの一意性を保証できます。

class Unique extends ThreadLocal<ClassLoader> implements Closable {
  @Override protected ClassLoader initialValue() {
    URL validation = Unique.class.getClassLoader()
                          .getResource("validation-model-1.1.6.jar");
    URL entry = Unique.class.getClassLoader()
                          .getResource("my-entry.jar");
    return new URLClassLoader(new URL[] {validation, entry}, null);
  }

  @Override public void close() throws IOException {
    get().close(); // If Java 7+, avoid handle leaks.
    set(null); // Make class loader eligable for GC.
  }

  public File doSomethingLibrary() throws Exception {
    Class<?> type = Class.forName("pkg.MyEntryPoint", false, get());
    return ((Callable<File>) type.newInstance()).call();
  }
}

クラスローダーは高価なオブジェクトであり、スレッドが存続している場合でも、クラスローダーが不要になったときに逆参照する必要があることに注意してください。また、ファイルリークを回避するには、逆参照する前にURLClassLoaderを閉じる必要があります。

最後に、Mavenの依存関係解決を引き続き使用し、コードを簡素化するために、エントリポイントコードを定義してMavenライブラリの依存関係を宣言する個別のMavenモジュールを作成できます。パッケージ化したら、Maven shadeプラグインを使用してUber jar を使用します。これには必要なものがすべて含まれています。このように、URLClassLoaderに単一のjarを提供するだけでよく、すべての(推移的な)依存関係を手動で確認する必要はありません。

3

この回答は、私の元の「プラグイン」コメントに基づいています。そして、それはブートと拡張クラスローダーからのみ継承するクラスローダーから始まります。

package safeLoaderPackage;

import Java.net.URL;
import Java.net.URLClassLoader;

public final class SafeClassLoader extends URLClassLoader{
    public SafeClassLoader(URL[] paths){
        super(paths, ClassLoader.getSystemClassLoader().getParent());
    }   
}

これは、ユーザーのクラスパスに含める必要がある唯一のクラスです。このURLクラスローダーは、ClassLoader.getSystemClassLoader()の親から継承します。ブートと拡張クラスローダーが含まれているだけです。ユーザーが使用するクラスパスの概念はありません。

package safeLoaderClasses;

import Java.net.URL;
import Java.util.ArrayList;
import Java.util.Collection;
import Java.util.concurrent.ArrayBlockingQueue;
import Java.util.concurrent.BlockingQueue;
import Java.util.concurrent.ThreadPoolExecutor;
import Java.util.concurrent.TimeUnit;

public class SecureClassLoaderPlugin <R> {

    private URL[] paths;
    private Class[] args;
    private String method;
    private String unsafe;

    public void setMethodData(final String u, final URL[] p, String m, Class[] a){
        method = m;
        args = a;
        paths = p;
        unsafe = u;
    }

    public Collection<R> processUnsafe(Object[][] p){
        int i;
        BlockingQueue<Runnable> q;
        ArrayList<R> results = new ArrayList<R>();
        try{
            i = p.length;
            q = new ArrayBlockingQueue<Runnable>(i);
            ThreadPoolExecutor tpe = new ThreadPoolExecutor(i, i, 0, TimeUnit.NANOSECONDS, q);
            for(Object[] params : p)
                tpe.execute(new SafeRunnable<R>(unsafe, paths, method, args, params, results));
            while(tpe.getActiveCount() != 0){
                Thread.sleep(10);
            }
            for(R r: results){
                System.out.println(r);
            }
            tpe.shutdown();
        }
        catch(Throwable t){

        }
        finally{

        }
        return results;
    }
}

そして

package safeLoaderClasses;

import Java.io.IOException;
import Java.lang.reflect.Method;
import Java.net.URL;
import Java.util.ArrayList;

import safeLoaderInterface.SafeClassLoader;

class SafeRunnable <R> implements Runnable{
    final URL[] paths;
    final private String unsafe;
    final private String method;
    final private Class[] args;
    final private Object[] processUs;
    final ArrayList<R> result;

    SafeRunnable(String u, URL[] p, String m, Class[] a, Object[] params, ArrayList<R> r){
        unsafe = u;
        paths = p;
        method = m;
        args = a;
        processUs = params;
        result = r;
    }

    public void run() {
        Class clazz;
        Object instance;
        Method m;
        SafeClassLoader sl = null;

        try{
            sl = new SafeClassLoader(paths);
            System.out.println(sl);

            clazz = sl.loadClass(unsafe);
            m = clazz.getMethod(method, args);
            instance = clazz.newInstance();
            synchronized(result){
                result.add((R) m.invoke(instance, processUs));
            }
        }
        catch(Throwable t){
            t.printStackTrace();
        }
        finally{
            try {
                sl.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

プラグインjarです。ラムダはありません。単なるスレッドプールエグゼキュータ。各スレッドは、実行後に結果リストに追加するだけです。

ジェネリックは磨く必要がありますが、私はこれらをこのクラスに対してテストしました(別の瓶にあります)

package stackoverflow4;

public final class CrazyClass {

    static int i = 0;

    public int returnInt(){
        System.out.println(i);
        return 8/++i;
    }
}

これは、自分のコードから接続する方法です。 getParent()呼び出しで失われるため、クラスローダーへのパスを含める必要があります

private void process(final String plugin, final String unsafe, final URL[] paths) throws Exception{
        Object[][] passUs = new Object[][] {{},{}, {},{}, {},{},{},{},{},{}};
        URL[] pathLoader = new URL[]{new File(new String(".../safeLoader.jar")).toURI().toURL(), 
                new File(new String(".../safeLoaderClasses.jar")).toURI().toURL()};
        //instantiate the loader
        SafeClassLoader sl = new SafeClassLoader(pathLoader); 
        System.out.println(sl);
        Class clazz = sl.loadClass("safeLoaderClasses.SecureClassLoaderPlugin");
        //Instance of the class that loads the unsafe jar and launches the thread pool executor
        Object o = clazz.newInstance(); 
        //Look up the method that set ups the unsafe library
        Method m = clazz.getMethod("setMethodData", 
                new Class[]{unsafe.getClass(), paths.getClass(), String.class, new Class[]{}.getClass()});
        //invoke it
        m.invoke(o, new Object[]{unsafe,paths,"returnInt", new Class[]{}});
        //Look up the method that invokes the library
        m = clazz.getMethod("processUnsafe", new Class[]{ passUs.getClass()});
        //invoke it
        o = m.invoke(o, passUs);
        //Close the loader
        sl.close();
    }

最大30以上のスレッドで動作するようです。プラグインは個別のクラスローダーを使用し、各スレッドは独自のクラスローダーを使用します。メソッドを終了した後、すべてがgcされます。

3
efekctive

「ライブラリを強化することは不可能です」が、カスタムクラスローダーのような流血の回避策を導入することは可能ですか?

OK。私は、元の質問への回答ではない回答を嫌う最初の人です。しかし、私は正直に言って、カスタムクラスローダーを導入するよりも、ライブラリにパッチを適用する方がはるかに簡単で管理しやすいと信じています。

ブロッカーはクラスorg.verapdf.gf.model.impl.containers.StaticContainersどのstaticフィールドは、以下に示すように、スレッドごとに機能するように簡単に変更できます。これは他の6つのクラスに影響します

org.verapdf.gf.model.GFModelParser
org.verapdf.gf.model.factory.colors.ColorSpaceFactory
org.verapdf.gf.model.impl.cos.GFCosFileSpecification
org.verapdf.gf.model.impl.external.GFEmbeddedFile
org.verapdf.gf.model.impl.pd.colors.GFPDSeparation
org.verapdf.gf.model.tools.FileSpecificationKeysHelper

スレッドごとに持つことができるPDFAParserは1つだけです。しかし フォーク 実行には10分かかり、基本的なマルチスレッドスモークテストで動作しました。これをテストして、ライブラリの元の作成者に連絡します。たぶん彼は喜んでマージし、更新されて管理されたライブラリへのMaven参照を保持することができます。

package org.verapdf.gf.model.impl.containers;

import org.verapdf.as.ASAtom;
import org.verapdf.cos.COSKey;
import org.verapdf.gf.model.impl.pd.colors.GFPDSeparation;
import org.verapdf.gf.model.impl.pd.util.TaggedPDFRoleMapHelper;
import org.verapdf.model.pdlayer.PDColorSpace;
import org.verapdf.pd.PDDocument;
import org.verapdf.pdfa.flavours.PDFAFlavour;

import Java.util.*;

public class StaticContainers {

    private static ThreadLocal<PDDocument> document;
    private static ThreadLocal<PDFAFlavour> flavour;

    // TaggedPDF
    public static ThreadLocal<TaggedPDFRoleMapHelper> roleMapHelper;

    //PBoxPDSeparation
    public static ThreadLocal<Map<String, List<GFPDSeparation>>> separations;
    public static ThreadLocal<List<String>> inconsistentSeparations;

    //ColorSpaceFactory
    public static ThreadLocal<Map<String, PDColorSpace>> cachedColorSpaces;

    public static ThreadLocal<Set<COSKey>> fileSpecificationKeys;

    public static void clearAllContainers() {
        document = new ThreadLocal<PDDocument>();
        flavour = new ThreadLocal<PDFAFlavour>();
        roleMapHelper = new ThreadLocal<TaggedPDFRoleMapHelper>();
        separations = new ThreadLocal<Map<String, List<GFPDSeparation>>>();
        separations.set(new HashMap<String,List<GFPDSeparation>>());
        inconsistentSeparations = new ThreadLocal<List<String>>();
        inconsistentSeparations.set(new ArrayList<String>());
        cachedColorSpaces = new ThreadLocal<Map<String, PDColorSpace>>();
        cachedColorSpaces.set(new HashMap<String,PDColorSpace>());
        fileSpecificationKeys = new ThreadLocal<Set<COSKey>>();
        fileSpecificationKeys.set(new HashSet<COSKey>());
    }

    public static PDDocument getDocument() {
        return document.get();
    }

    public static void setDocument(PDDocument document) {
        StaticContainers.document.set(document);
    }

    public static PDFAFlavour getFlavour() {
        return flavour.get();
    }

    public static void setFlavour(PDFAFlavour flavour) {
        StaticContainers.flavour.set(flavour);
        if (roleMapHelper.get() != null) {
            roleMapHelper.get().setFlavour(flavour);
        }
    }

    public static TaggedPDFRoleMapHelper getRoleMapHelper() {
        return roleMapHelper.get();
    }

    public static void setRoleMapHelper(Map<ASAtom, ASAtom> roleMap) {
        StaticContainers.roleMapHelper.set(new TaggedPDFRoleMapHelper(roleMap, StaticContainers.flavour.get()));
    }
}
1
Serg M Ten

回避策を探す前に、問題の修正を試みる必要があると思います。

コードは、クラスローダー、プロセス、コンテナー、VM、またはマシンの2つのスレッドでいつでも実行できます。しかし、それらは理想的なものではありません。

コードから2つのdefaultInstance()を見ました。インスタンスはスレッドセーフですか?そうでない場合、2つのインスタンスを持つことができますか?それは工場ですか、それともシングルトンですか?

第二に、紛争はどこで起こりますか?初期化/キャッシュの問題に関する場合は、事前ウォーミングで修正する必要があります。

最後になりましたが、ライブラリがオープンソースの場合は、フォークで修正し、リクエストをプルします。

1
Dennis C