私は、スレッドセーフが保証されていない(そしてそうではない)ライブラリを使用し、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です。
このクラスローダーコードを「現状のまま」投稿しても、あまり意味がありません(そして、いくつかの著作権の問題が発生します)。そのルートをさらに探索したい場合は、スケルトンを考え出すことができます。
答えは、実際にはライブラリが何に依存しているかによって異なります。
ClassLoader
sを使用してライブラリのコードを分離しても、 JNI仕様 によると、同じJNIネイティブライブラリを複数にロードすることは許可されていないため、役に立ちません。最終的にUnsatisfiedLinkError
になるような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
を呼び出す必要があると仮定します。対応するMethod
をThreadLocal
に格納して、次のように 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();
}
}
);
_
私は質問が興味深いことに気づき、あなたのために小さなツールを作成しました:
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>
_
このスレッドの他の部分で説明されているクラスのロード、オブジェクトのインスタンス化、およびプロキシ生成機能をすでに提供しているため、内部で 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();
}
}
_
スレッドごとにクラスローダーでライブラリを分離することにより、提案したとおりにクラスの同時実行プロパティを保証できます。唯一の例外は、bootstrapクラスローダーまたはシステムクラスローダーと明示的に相互作用するライブラリです。リフレクションまたはInstrumentation
APIのいずれかによって、これらのクラスローダーにクラスを挿入できます。 。そのような機能の1つの例は、Mockitoのインラインモックメーカーですが、私の知る限り、同時実行の制約はありません。
この動作でクラスローダーを実装することは、それほど難しいことではありません。最も簡単な解決策は、プロジェクトに必要なjarを明示的に含めることです。リソースとして。このように、クラスをロードするためにURLClassLoader
を使用できます。
URL url = getClass().getClassLoader().getResource("validation-model-1.1.6.jar");
ClassLoader classLoader = new URLClassLoader(new URL[] {url}, null);
null
をURLClassLoader
(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を提供するだけでよく、すべての(推移的な)依存関係を手動で確認する必要はありません。
この回答は、私の元の「プラグイン」コメントに基づいています。そして、それはブートと拡張クラスローダーからのみ継承するクラスローダーから始まります。
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されます。
「ライブラリを強化することは不可能です」が、カスタムクラスローダーのような流血の回避策を導入することは可能ですか?
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()));
}
}
回避策を探す前に、問題の修正を試みる必要があると思います。
コードは、クラスローダー、プロセス、コンテナー、VM、またはマシンの2つのスレッドでいつでも実行できます。しかし、それらは理想的なものではありません。
コードから2つのdefaultInstance()を見ました。インスタンスはスレッドセーフですか?そうでない場合、2つのインスタンスを持つことができますか?それは工場ですか、それともシングルトンですか?
第二に、紛争はどこで起こりますか?初期化/キャッシュの問題に関する場合は、事前ウォーミングで修正する必要があります。
最後になりましたが、ライブラリがオープンソースの場合は、フォークで修正し、リクエストをプルします。