Подтвердить что ты не робот

Хотите параллельно запускать нетекающую библиотеку - можно ли это сделать с помощью нескольких загрузчиков классов?

Я работаю над проектом, в котором мы используем библиотеку, которая не гарантируется потокобезопасностью (и не является) и однопоточной в сценарии потоков 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>

В настоящее время невозможно закрепить библиотеку.


EDIT: меня попросили показать код. Наш основной адаптер примерно:

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).

(Увидев код, я заметил, что есть вызов инициализатора в конструкторе, который может быть виновником здесь в моем конкретном случае. Мне все равно хотелось бы решить общую проблему.

4b9b3361

Ответ 1

Мы столкнулись с подобными проблемами. Проблемы обычно исходили из статических свойств, которые стали неохотно "разделяться" между различными потоками.

Использование разных загрузчиков классов работало для нас до тех пор, пока мы могли гарантировать, что статические свойства действительно были установлены на классы, загруженные нашим загрузчиком классов. Java может иметь несколько классов, которые предоставляют свойства или методы, которые не изолированы между потоками или не являются потокобезопасными ("System.setProperties() и Security.addProvider() в порядке - любая каноническая документация по этому вопросу приветствуется btw).

Потенциально работоспособное и быстрое решение, которое по крайней мере может дать вам возможность протестировать эту теорию для вашей библиотеки, - это использовать механизм сервлетов, такой как Jetty или Tomcat.

Создайте несколько войн, которые содержат вашу библиотеку и запускают параллельные процессы (1 на войну).

При запуске кода внутри потока сервлета, WebappClassLoaders этих движков пытается сначала загрузить классы из загрузчика родительского класса (то же, что и движок), и если он не находит класс, пытается загрузить его из банки/классы, упакованные с войной.

С причалом вы можете программно раскручивать войны в соответствии с вашим выбором, а затем теоретически масштабировать количество процессоров (войн) по мере необходимости.

Мы реализовали наш собственный загрузчик классов, расширив URLClassLoader и вдохнув вдохновение из Jetty Webapp ClassLoader. Это не такая трудная работа, как кажется.

Наш загрузчик классов делает совершенно противоположное: он пытается загрузить класс из локальных банок в "пакет" первый, а затем пытается получить их из загрузчика родительского класса. Это гарантирует, что библиотека, случайно загруженная родительским загрузчиком классов, никогда не рассматривается (сначала). Наш "пакет" на самом деле представляет собой банку, содержащую другие банки/библиотеки с настраиваемым файлом манифеста.

Проводка этого кода загрузчика этого класса "как есть" не будет иметь большого смысла (и создать несколько проблем с авторским правом). Если вы хотите изучить этот маршрут дальше, я могу попытаться придумать скелет.

Источник Jetty WebappClassLoader

Ответ 2

Ответ на самом деле зависит от того, на что полагается ваша библиотека:

  • Если ваша библиотека опирается, по крайней мере, на одну собственную библиотеку, использование ClassLoader для изоляции вашего кода библиотеки не поможет, потому что согласно Спецификация JNI, нельзя загружать одну и ту же родную библиотеку JNI в более чем один загрузчик классов, так что вы получите UnsatisfiedLinkError.
  • Если вы используете библиотеку, по крайней мере, один внешний ресурс, который не предназначен для совместного использования, например, например, файл и который изменен вашей библиотекой, вы можете столкнуться с сложными ошибками и/или повреждением ресурса.

Предполагая, что вы не находитесь в перечисленных выше случаях, вообще говоря, если класс известен как non thread safe и не изменяет никаких статических полей, использование выделенного экземпляра этого класса для каждого вызова или для потока достаточно хорошо, тогда экземпляр класса не будет более общим.

Здесь, поскольку ваша библиотека явно полагается и модифицирует некоторые статические поля, которые не предназначены для совместного использования, вам действительно нужно изолировать классы вашей библиотеки в выделенном 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) следующим образом:

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
...

Поскольку этот подход создаст один 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();
        }
    }
);

Ответ 3

Я нашел интересующий вопрос и создал для вас небольшой инструмент:

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"
        );
    }
  }
}

Я проверил свою концепцию с помощью нескольких unit и интеграция тесты, среди которых один, показывающий, как воспроизводит и решает проблему veraPDF.

Вот как выглядит ваш код при использовании моего специального загрузчика классов:

Класс VeraPDFValidator:

Мы просто добавляем член static ThreadLocal<ThreadSafeClassLoader> к нашему классу, сообщая ему, какие классы/библиотеки должны помещаться в новый загрузчик классов (достаточно указать один класс на библиотеку, впоследствии мой инструмент автоматически идентифицирует библиотеку).

Затем через threadSafeClassLoader.get().newObject(forTargetType(VeraPDFValidatorHelper.class)) мы создаем экземпляр нашего вспомогательного класса внутри потокобезопасного загрузчика классов и создаем для него прокси-объект, чтобы мы могли его вызвать извне.

BTW, 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();
  }
}

Ответ 4

Отделяя библиотеку от загрузчика класса за поток, вы можете гарантировать любые классы concurrency, как вы предлагаете. Единственным исключением являются библиотеки, которые явно взаимодействуют с загрузчиком класса загрузки или загрузчиком системного класса. Можно вводить классы в эти загрузчики классов с помощью отражения или API Instrumentation. Одним из примеров такой функциональности будет Mockito inline mock maker, который, как мне известно, не страдает от ограничения concurrency.

Реализация загрузчика классов с таким поведением не слишком сложна. Самым простым решением было бы явно включать в проект необходимые банки, например. как ресурс. Таким образом, вы можете использовать URLClassLoader для загрузки ваших классов:

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

Отсылая null как суперкласс-загрузчик URLClassLoader (второй аргумент), вы гарантируете, что не существует общих классов за пределами классов начальной загрузки. Обратите внимание, что вы не можете использовать какие-либо классы этого созданного загрузчика классов извне. Однако, если вы добавите вторую банку, содержащую класс, который запускает вашу логику, вы можете предложить точку входа, которая становится доступной без отражения:

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

Просто добавьте этот класс в свою банку и поставьте его как второй элемент в вышеприведенный массив URL. Обратите внимание, что вы не можете ссылаться на тип библиотеки как возвращаемое значение, так как этот тип не будет доступен для пользователя, который живет за пределами загрузчика классов, который использует точку входа.

Обернув создание загрузчика класса в ThreadLocal, вы можете гарантировать, что загрузчики классов uniqunes:

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 чтобы создать банку Uber, которая включает в себя все, что вам нужно. Таким образом, вам нужно предоставить только одну банку для вашего URLClassLoader и не нужно обеспечивать все (переходные) зависимости вручную.

Ответ 5

Этот ответ основан на моем оригинальном комментарии к плагину. И он начинается с загрузчика классов, который наследуется только от загрузчиков классов загрузки и расширений.

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();
            }
        }
    }
}

- это плагин. Нет лямбдов. Просто исполнитель потока. Каждый поток просто добавляет в список результатов после выполнения.

Генераторы нуждаются в полировке, но я тестировал их против этого класса (находится в другой банке)

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+ и, похоже, работает. Плагин использует отдельный загрузчик классов, и каждый из потоков использует свой собственный загрузчик классов. После того, как вы покинули метод, все будет записано.

Ответ 6

Я считаю, что вы должны попытаться устранить проблему, прежде чем искать способ обхода.

Вы всегда можете запустить код в двух потоках, загрузчиках классов, процессах, контейнерах, виртуальных машинах или машинах. Но они не идеальны.

Я видел два кода defaultInstance() из кода. Делаете ли он потоки? Если нет, можем ли мы иметь два экземпляра? Это factory или синглтон?

Во-вторых, где происходят конфликты? Если речь идет о проблеме инициализации/кэширования, рекомендуется исправить предварительное нагревание.

И последнее, но не менее важное: если библиотека была с открытым исходным кодом, fork ее исправить и вытащить запрос.

Ответ 7

"Невозможно упростить библиотеку", но возможно ввести такой кровавый обходной путь, как пользовательский загрузчик классов?

OK. Я первый, кто не любит ответы, которые не являются ответом на исходный вопрос. Но я искренне верю, что исправление библиотеки намного проще и удобнее, чем введение пользовательского загрузчика классов.

Блокатором является класс org.verapdf.gf.model.impl.containers.StaticContainers, который может быть легко изменен для полей static для работы в потоке, как показано ниже. Это влияет на шесть других классов

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 на поток. Но fork занимает десять минут и работает для меня в основном многопоточном smoke test. Я бы проверил это и связался с оригинальным автором библиотеки. Возможно, он счастлив слиться, и вы можете просто сохранить ссылку 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()));
    }
}